Compare commits

...
Author SHA1 Message Date
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
781 changed files with 25963 additions and 35490 deletions
+11 -2
View File
@@ -281,10 +281,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
@@ -335,6 +339,8 @@ jobs:
run: >-
npx vitest run --coverage.enabled=false
tests/boundary/filesystem/binaryTarget.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
@@ -345,6 +351,8 @@ jobs:
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:package:windows
- uses: actions/upload-artifact@v4
@@ -366,6 +374,7 @@ jobs:
cache: npm
- run: npm ci
- run: npm run docs:check
- run: npm run docs:api:cached
- uses: actions/upload-artifact@v4
with:
name: api-docs
@@ -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"
+4 -1
View File
@@ -7,7 +7,7 @@ __pycache__/
dist/
build/
docs/api/
node_modules/
node_modules
*.egg-info/
*.egg
@@ -42,3 +42,6 @@ test-results.xml
# OS
.DS_Store
Thumbs.db
# Local reference sources and analysis artifacts
/_reference/
+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/**"
]
}
-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.0"
}
+26 -7
View File
@@ -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:api:cached` separately renders API HTML when needed.
- 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,7 +87,7 @@ 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.
+127
View File
@@ -1,5 +1,132 @@
# Changelog
## [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 -28
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
@@ -22,36 +22,45 @@ worktrees. After a package, lockfile, or managed-skill version change, run
Keep dependencies flowing inward through the existing domain, contracts, provider, application, server, and adapter layers. Parse unknown values at process and protocol boundaries, model expected failures with `Result`, and preserve the canonical tool inventory defined by `TOOL_CONTRACTS` unless a deliberate contract change updates every verifier, generated catalog artifact, and snapshot. Prefer capability- and session-scoped tool advertisement over schema truncation.
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 without rendering API HTML.
`docs:generate` regenerates those files; render API HTML separately with
`npm run docs:api:cached`. PR CI renders and uploads the `api-docs` artifact.
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 +101,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`, `npm run docs:api:cached`, 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 +127,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
```
+181 -332
View File
@@ -13,44 +13,52 @@
[![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)
[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 and macOS, 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 +87,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, 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)
- 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-read-only-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 and currently unavailable. The required Windows process ownership, private-directory permissions, and safe-path checks are not implemented. See [Windows Ghidra P0](docs/windows-ghidra-p0.md) for the remaining requirements.
If something is not working, run:
@@ -199,13 +157,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 +178,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
The Ghidra adapter supports the exact official Ghidra 12.1.4 release with a 64-bit full JDK 21 on Linux x64 and macOS x64/arm64. macOS additionally requires the matching native Ghidra decompiler; the adapter does not build or install it. 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 +196,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 **22 read-only operations** for functions, strings, symbols, assembly, decompilation, calls, references, instructions, and data types. These also support REA's overview, search, call-graph, and function-analysis workflows. Ghidra does not provide GUI controls or annotation changes 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 and macOS use 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 22 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`, `analyze_function`, `inspect_native_instruction`, `resolve_native_call_targets`, and `inspect_native_data_type`. The exact-object primitives report instruction tokens, static call targets, and recovered database layouts; unsupported facets remain explicit. `trace_native_values` composes bounded decompiler-derived argument/parameter and return/output dependencies across resolved calls. `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 host-native fixtures (Linux x64 ELF or macOS Mach-O) and a native DWARF 4 type-layout object. 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 the Windows P0 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 operations remain unavailable pending the controls described in 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 +217,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 +233,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 +245,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 +261,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 +286,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,157 +299,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 approved native desktop observation.
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 | 39 | procedures, pseudocode, assembly, strings, names, segments, callers, callees, xrefs, annotations, bounded byte reads, file-offset translation |
| Investigation workflows | 15 | `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 | 7 | Mach-O metadata, code signatures, plists, architectures, Swift demangling; Hopper-free and provenance-bearing |
| Artifact graph | 5 | complete inline inspection of directories and supported packages, compiled Interface Builder UI graph decoding, paginated Apple asset catalog metadata, 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 | 39 | 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 operations are currently unavailable. 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.
## 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
@@ -517,7 +422,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.0", "mcp"]
}
}
}
@@ -526,11 +431,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
@@ -586,7 +491,7 @@ Or install the `rea` command globally:
```bash
npm install --global rea-agents
rea --help
rea upgrade
rea update
rea mcp
```
@@ -594,8 +499,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
@@ -612,47 +516,27 @@ 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 exposes 22 read-only operations on supported Linux and macOS hosts. GUI controls and annotation changes require Hopper. Windows Ghidra operations remain unavailable.
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`
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
@@ -665,28 +549,20 @@ rea inventory-artifact ./app.asar --json | jq . > inventory.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:
@@ -697,9 +573,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,
@@ -716,7 +592,11 @@ verified or absent.
## Security model
REA does not provide a hosted analysis service. Hopper and POSIX 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 operations are blocked until REA implements the required process ownership, private-directory permissions, and safe-path checks. Report vulnerabilities through the private process in [SECURITY.md](SECURITY.md).
## FAQ
@@ -730,7 +610,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>
@@ -744,7 +624,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-read-only-analysis-provider).
</details>
@@ -765,48 +645,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. PR CI publishes generated API documentation as the `api-docs` artifact.
`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
+124 -85
View File
@@ -4,7 +4,7 @@
# REA: هندسة أي شيء عكسيًا
### واجهة CLI وخادم MCP واحدان يمكّنان وكلاء البرمجة من إجراء هندسة عكسية لأي شيء
### استقصِ سلوك التطبيقات والملفات التنفيذية الأصلية باستخدام وكيلك.
**اعثر على ميزة تعجبك. افهم آلية عملها. ابنها بالطريقة التي تريدها.**
@@ -13,109 +13,150 @@
[![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)
[البدء السريع](#البدء-السريع) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [دليل الأدوات](#منصة-أدوات-التحقيق) · [كيف يعمل](#كيف-يعمل) · [الأسئلة الشائعة](#الأسئلة-الشائعة)
[البدء السريع](#البدء-السريع) · [حالة الدعم](#حالة-الدعم) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [منصة أدوات التحقيق](#منصة-أدوات-التحقيق) · [خطة العمل](#خطة-العمل) · [كيف يعمل؟](#كيف-يعمل)
<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 و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.19+ أو 24.11+ و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 +200,26 @@ rea uninstall --purge-data
## منصة أدوات التحقيق
| عائلة الأدوات | العدد | أمثلة |
| --------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| فحص الملفات التنفيذية | 39 | الدوال، والشيفرة شبه المصدرية، والتعليمات، والسلاسل، والأسماء، والمراجع، والتعليقات |
| التحليل المركب | 15 | النظرة العامة، و`inspect_native_api`، وفك الترجمة الدفعي، ومخططات الاستدعاء، والمراجع، واكتشاف الأنواع |
| أدوات macOS الأصلية | 7 | بيانات Mach-O والتوقيعات وملفات plist والمعماريات وفك رموز Swift دون تشغيل Hopper |
| رسم القطع الأثرية | 5 | فحص الدلائل والحزم، ورسم واجهة Interface Builder، وبيانات كتالوج أصول Apple، والاستخراج المحدد صراحة |
| Managed PE/CLI | 8 | هوية PE/CLI، أعضاء البيانات الوصفية، بصمات CIL، تصريحات P/Invoke/الحدود الأصلية والتحقق منها، إسقاط رسم التطبيق، استيراد إعادة البناء من فك الترجمة، إعادة ربط الرموز، خطط ترابط وقت التشغيل ومقارنة الإصدارات |
| مراقبة المتصفح | 9 | التقاط CDP محدود الأصل وتحليل الحزم وخرائط المصدر واكتشاف WebMCP والفروق والأدلة المرئية |
| تحليل Electron | 5 | مراقبة سلبية ضمن جذور canonical، ورسم ثابت ومحدود للتطبيق، ومطابقة ثابتة/وقت تشغيل قائمة على Evidence |
| وقت تشغيل JavaScript | 2 | اكتشاف أهداف Node/Electron Inspector المعتمدة للاتصال فقط، ومراقبة محدودة للبرامج النصية وسياقات التنفيذ دون تقييم أو تجهيز |
| سير عمل التطبيقات | 10 | تتبع محدود ومقارنة إصدارات فريدة ومقارنة ثابتة لأشكال قيم إرجاع الصادرات وإعادة تشغيل وحدات معزولة ومعتمدة على Linux وتوصيف وقت التشغيل المُدار وإغلاق تغطية إعادة البناء ودفاتر التزامات حتمية |
| جلسة الملف التنفيذي | 22 | فتح الأهداف وحزم الأدلة وتشغيل آلة إعادة محدودة ومقارنة العمليات والقطع والدوال وسجل المجهولات المتبقية |
| عائلة الأدوات | العدد | الاستخدام |
| --------------------- | ----: | -------------------------------------------------------------------------------------- |
| فحص الملفات التنفيذية | 39 | الدوال والشيفرة شبه المصدرية والتعليمات والسلاسل والرموز والمراجع والتعليقات |
| التحليل المركب | 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 وVS Code. تكون تسجيلات REA الموجودة محددة افتراضياً، ويجب اختيار العملاء المكتشفين الآخرين يدوياً. يمكن لأي وكيل يدعم خوادم MCP المحلية الاتصال باستخدام الإعداد التالي:
<!-- x-release-please-start-version -->
@@ -183,7 +228,7 @@ rea uninstall --purge-data
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@3.2.1", "mcp"]
"args": ["-y", "rea-agents@4.0.0", "mcp"]
}
}
}
@@ -204,7 +249,7 @@ flowchart LR
Ghidra --> App
```
يستخدم CLI وخادم MCP محرك التحليل نفسه. تغلق أوامر الطرفية التطبيق عند انتهائها، بينما تبقيه جلسة الوكيل مفتوحًا أثناء الاستقصاء.
يستخدم CLI وخادم MCP سير عمل التحليل نفسه. تحرر أوامر الطرفية جلسة الجسر الخاصة بها عند انتهائها، بينما يمكن لجلسة الوكيل الحفاظ على الاتصال أثناء الاستقصاء. إغلاق جلسة REA لا يغلق تطبيق Hopper الذي يستخدمه المستخدم.
## أوامر CLI
@@ -221,15 +266,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 +297,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>
+119 -75
View File
@@ -4,7 +4,7 @@
# REA:あらゆるものをリバースエンジニアリング
### コーディングエージェントがあらゆるものをリバースエンジニアリングするための、ひとつの CLI / MCP サーバー
### エージェントでアプリの動作からネイティブバイナリまで調べる。
**気になる機能を見つけ、仕組みを理解し、自分の形で実装する。**
@@ -13,35 +13,49 @@
[![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)
[クイックスタート](#クイックスタート) · [現在の対応状況](#現在の対応状況) · [バイナリから動作へ](#バイナリから動作へ) · [調査ツールカタログ](#調査ツールカタログ) · [ロードマップ](#ロードマップ) · [仕組み](#仕組み)
<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 +67,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、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.19+ または 24.11+ と 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 +189,26 @@ REA は手順 1〜5 のバイナリ解析を処理し、手順 6 はエージェ
## 調査ツールカタログ
| ツール群 | 数 | 例 |
| ---------------------- | --: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| バイナリ調査 | 39 | プロシージャ、疑似コード、アセンブリ、文字列、名前、セグメント、callers、callees、xrefs、注釈 |
| 合成解析 | 15 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, Swift / ObjC 検出 |
| macOS ネイティブ | 7 | Mach-O メタデータ、署名、plist、アーキテクチャ、Swift デマングル。Hopper 起動不要 |
| アーティファクトグラフ | 5 | ディレクトリと対応パッケージの調査、Interface Builder UI グラフ、Apple アセットカタログのメタデータ、明示選択された抽出 |
| 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`、証拠バンドル、有限リプレイ状態機械、プロセス/成果物/関数比較、残余未知レジストリ |
| ツール分類 | 数 | 用途 |
| -------------------- | --: | --------------------------------------------------------------------------- |
| バイナリ検査 | 39 | 関数、疑似コード、アセンブリ、文字列、シンボル、参照、注釈 |
| 組み合わせた解析 | 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、VS Code に対応します。既存の REA 登録は初期選択され、それ以外の検出済みクライアントは選択が必要です。ローカル MCP サーバーに対応するエージェントは、次の設定でも接続できます。
<!-- x-release-please-start-version -->
@@ -173,7 +217,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.0", "mcp"]
}
}
}
@@ -193,7 +237,7 @@ flowchart LR
Ghidra --> App
```
CLI と MCP サーバーは同じ解析エンジンを使います。ターミナルのコマンドは終了時にアプリを閉じ、エージェントのセッションは調査中アプリを開いたままにします。
CLI と MCP サーバーは同じ解析処理を使います。ターミナルコマンドは完了後に自分のブリッジセッションを解放し、エージェントセッションは調査中の接続を維持できます。REA セッションを閉じても、ユーザーが利用中の Hopper は終了しません。
## CLI
@@ -229,7 +273,7 @@ REA は必要なときに Hopper を起動します。Hopper のランチャー
<details><summary><strong>Hopper を先に起動する必要がありますか?</strong></summary>
いいえ。REA が必要時に起動します。すでに起動している Hopper にも対応します。
いいえ。REA が必要時に起動します。Hopper が起動済みでも使えますが、既存の GUI ドキュメントには接続せず、新しい解析ドキュメントを開きます。
</details>
+120 -75
View File
@@ -4,7 +4,7 @@
# REA: 무엇이든 리버스 엔지니어링
### 코딩 에이전트가 무엇이든 리버스 엔지니어링할 수 있게 하는 하나의 CLI 및 MCP 서버
### 에이전트로 앱의 동작부터 네이티브 바이너리까지 조사하세요.
**마음에 드는 기능을 찾고, 작동 방식을 이해하고, 원하는 방식으로 구현하세요.**
@@ -13,35 +13,50 @@
[![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)
[빠른 시작](#빠른-시작) · [현재 지원 범위](#현재-지원-범위) · [바이너리에서 동작까지](#바이너리에서-동작까지) · [조사 도구 카탈로그](#조사-도구-카탈로그) · [로드맵](#로드맵) · [작동 방식](#작동-방식)
<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 +68,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, 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.19+ 또는 24.11+와 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 +190,26 @@ REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이
## 조사 도구 카탈로그
| 도구 그룹 | 수 | 예시 |
| ------------------- | --: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 바이너리 검사 | 39 | 프로시저, 의사 코드, 어셈블리, 문자열, 이름, 세그먼트, callers, callees, xrefs, 주석 |
| 합성 분석 | 15 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, Swift 및 ObjC 탐색 |
| macOS 네이티브 도구 | 7 | Mach-O 메타데이터, 코드 서명, plist, 아키텍처, Swift 디맹글링. Hopper 실행 불필요 |
| 아티팩트 그래프 | 5 | 디렉터리와 지원 패키지 검사, Interface Builder UI 그래프, Apple 에셋 카탈로그 메타데이터, 명시적으로 선택한 추출 |
| 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 상태 머신, 프로세스·아티팩트·함수 비교, 잔여 미지수 레지스트리 |
| 도구 분류 | 수 | 용도 |
| ----------------- | --: | -------------------------------------------------------------------- |
| 바이너리 검사 | 39 | 함수, 의사 코드, 어셈블리, 문자열, 심볼, 참조, 주석 |
| 결합 분석 | 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, VS Code를 지원합니다. 기존 REA 등록은 기본 선택되고, 그 밖의 감지된 클라이언트는 직접 선택해야 합니다. 로컬 MCP 서버를 지원하는 에이전트는 다음 설정으로 연결할 수 있습니다.
<!-- x-release-please-start-version -->
@@ -173,7 +218,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.0", "mcp"]
}
}
}
@@ -193,7 +238,7 @@ flowchart LR
Ghidra --> App
```
CLI와 MCP 서버는 같은 분석 엔진을 사용합니다. 터미널 명령은 끝날 때 앱을 닫고, 에이전트 세션은 조사하는 동안 앱을 열어 둡니다.
CLI와 MCP 서버는 같은 분석 워크플로를 사용합니다. 터미널 명령은 완료 후 자신의 브리지 세션을 해제하고, 에이전트 세션은 조사 중 연결을 유지할 수 있습니다. REA 세션을 닫아도 사용자가 이용 중인 Hopper 앱은 종료하지 않습니다.
## CLI
@@ -229,7 +274,7 @@ REA는 필요할 때 Hopper를 시작합니다. Hopper 런처는 내부적으로
<details><summary><strong>Hopper를 미리 실행해야 하나요?</strong></summary>
아니요. REA가 필요할 때 시작하며 이미 실행 중인 Hopper도 지원합니다.
아니요. REA가 필요할 때 시작합니다. Hopper가 실행 중이어도 사용할 수 있지만 기존 GUI 문서에 연결하는 대신 새 분석 문서를 엽니다.
</details>
+120 -76
View File
@@ -4,7 +4,7 @@
# REA:逆向工程一切
### 一个 CLI 与 MCP 服务器,让编程智能体逆向工程任何程序
### 使用智能体逆向工程任何程序,从应用行为到原生二进制文件。
**看到喜欢的功能。理解它的原理。按照你的方式实现。**
@@ -13,35 +13,49 @@
[![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)
[快速开始](#快速开始) · [从二进制到行为](#从二进制到行为) · [工具目录](#调查工具目录) · [工作原理](#工作原理) · [常见问题](#常见问题)
[快速开始](#快速开始) · [当前状态](#当前状态) · [从二进制到行为](#从二进制到行为) · [调查工具目录](#调查工具目录) · [路线图](#路线图) · [工作原理](#工作原理)
<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 +67,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 和 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.19+ 或 24.11+,以及 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 +189,26 @@ REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常
## 调查工具目录
| 工具类别 | 数量 | 示例 |
| ----------------- | ---: | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| 二进制检查 | 39 | 过程、伪代码、汇编、字符串、名称、段、调用者、被调用者、交叉引用、注释 |
| 组合分析 | 15 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`、Swift 与 ObjC 发现 |
| macOS 原生工具 | 7 | Mach-O 元数据、代码签名、plist、架构与 Swift 符号还原,无需启动 Hopper |
| 制品图谱 | 5 | 目录及支持的软件包内联检查、Interface Builder UI 图解、Apple 资源目录元数据分页检查,以及显式选择的事务式提取 |
| 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`、证据包、有限重放状态机、进程、制品与函数比较、残余未知项注册表 |
| 工具类别 | 数量 | 用途 |
| ----------------- | ---: | --------------------------------------------------------- |
| 二进制检查 | 39 | 函数、伪代码、汇编、字符串、符号、段、引用与注释 |
| 组合分析 | 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 和 VS Code。已有 REA 注册默认选中;其他检测到的客户端需手动选择。任何支持本地 MCP 服务器的智能体都可以使用以下配置连接 REA。
<!-- x-release-please-start-version -->
@@ -173,7 +217,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.0", "mcp"]
}
}
}
@@ -193,7 +237,7 @@ flowchart LR
Ghidra --> App
```
CLI 与 MCP 服务器使用相同的分析引擎。终端命令完成后会关闭应用;智能体会话则会在调查期间保持应用打开。
CLI 与 MCP 服务器使用相同的分析流程。终端命令完成后释放自己创建的桥接会话;智能体会话可以在调查期间保持连接。关闭 REA 会话不会退出用户正在使用的 Hopper 应用。
## CLI
@@ -223,7 +267,7 @@ REA 会推导明确的格式和架构参数,以避免常见的 FAT 与 ARM 选
## 安全模型
每个桥会话都使用随机能力令牌和仅限当前用户的 Unix 套接字。协议消息有大小限制,面向调用者的错误不会暴露启动器 stderr 或内部异常原因。Ghidra 会话还使用隔离的临时项目,不会打开或修改用户自己的 Ghidra 项目。
每个桥会话都使用随机能力令牌和仅限当前用户的 Unix 套接字。诊断保留本机路径、摘要和错误位置等排查信息,同时移除凭据和认证令牌。Ghidra 会话还使用隔离的临时项目,不会打开或修改用户自己的 Ghidra 项目。
这不是沙箱,也无法防御以同一操作系统用户身份运行的恶意进程。打开不可信二进制文件会让所选本地提供商以当前用户权限进行解析和分析。请按照 [SECURITY.md](SECURITY.md) 中的私密流程报告漏洞。
@@ -231,7 +275,7 @@ REA 会推导明确的格式和架构参数,以避免常见的 FAT 与 ARM 选
<details><summary><strong>Hopper 是否需要提前运行?</strong></summary>
不需要。REA 会在操作需要时启动 Hopper,也支持已经运行的 Hopper。
不需要。REA 会在操作需要时启动 Hopper。Hopper 已经运行时也可以使用,但 REA 会打开新的分析文档,不会连接并复用现有 GUI 文档。
</details>
+251 -32
View File
@@ -48,9 +48,13 @@ import ghidra.app.decompiler.DecompInterface;
import ghidra.app.decompiler.DecompileOptions;
import ghidra.app.decompiler.DecompileResults;
import ghidra.app.decompiler.DecompiledFunction;
import ghidra.app.decompiler.ClangCaseToken;
import ghidra.app.decompiler.ClangNode;
import ghidra.framework.Application;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSpace;
import ghidra.program.model.address.AddressRange;
import ghidra.program.model.address.AddressRangeIterator;
import ghidra.program.model.block.BasicBlockModel;
import ghidra.program.model.block.CodeBlock;
import ghidra.program.model.block.CodeBlockIterator;
@@ -82,6 +86,8 @@ import ghidra.program.model.pcode.HighSymbol;
import ghidra.program.model.pcode.JumpTable;
import ghidra.program.model.pcode.PcodeOp;
import ghidra.program.model.pcode.PcodeOpAST;
import ghidra.program.model.pcode.PcodeBlock;
import ghidra.program.model.pcode.PcodeBlockBasic;
import ghidra.program.model.pcode.Varnode;
import ghidra.program.model.pcode.VarnodeAST;
@@ -372,6 +378,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
decompiler.setOptions(options);
decompiler.toggleCCode(true);
decompiler.toggleSyntaxTree(true);
decompiler.toggleJumpLoads(true);
if (!decompiler.openProgram(currentProgram)) {
throw new IllegalStateException("Ghidra decompiler could not open the imported Program");
}
@@ -440,7 +447,9 @@ public final class ReaGhidraBridge extends HeadlessScript {
result.addProperty("name", procedureName(function));
result.addProperty("entrypoint", canonicalAddress(function.getEntryPoint()));
result.addProperty("basicblock_count", basicBlocks(function).size());
result.addProperty("length", function.getBody().getNumAddresses());
JsonObject body = functionBody(function);
result.addProperty("length", body.get("total_bytes").getAsLong());
result.add("body", body);
result.addProperty("signature", function.getPrototypeString(false, true));
result.add("locals", functionLocals(function));
result.add("classification", functionClassification(function));
@@ -774,6 +783,10 @@ public final class ReaGhidraBridge extends HeadlessScript {
limitations.add(
"Synthetic Ghidra entry-point references without actionable memory sources are omitted."
);
if (decompilation != null && decompilation.getErrorMessage() != null &&
!decompilation.getErrorMessage().isBlank()) {
limitations.add("Ghidra decompiler diagnostic: " + decompilation.getErrorMessage());
}
result.add("limitations", limitations);
return result;
}
@@ -1346,8 +1359,12 @@ public final class ReaGhidraBridge extends HeadlessScript {
}
JumpTable[] recoveredJumpTables = highFunction.getJumpTables();
List<ClangNode> caseTokens = new ArrayList<>();
if (decompilation.getCCodeMarkup() != null) {
decompilation.getCCodeMarkup().flatten(caseTokens);
}
for (int index = 0; index < recoveredJumpTables.length; index += 1) {
jumpTables.add(inferredJumpTable(recoveredJumpTables[index]));
jumpTables.add(inferredJumpTable(recoveredJumpTables[index], caseTokens));
}
return result;
}
@@ -1443,7 +1460,10 @@ public final class ReaGhidraBridge extends HeadlessScript {
return result;
}
private JsonObject inferredJumpTable(JumpTable jumpTable) throws Exception {
private JsonObject inferredJumpTable(
JumpTable jumpTable,
List<ClangNode> caseTokens
) throws Exception {
JsonObject result = new JsonObject();
result.addProperty(
"dispatch_address",
@@ -1542,13 +1562,33 @@ public final class ReaGhidraBridge extends HeadlessScript {
}
result.add("data_sources", dataSources);
Address[] targets = jumpTable.getCases();
Integer[] labels = jumpTable.getLabelValues();
boolean labelsAligned = labels != null && labels.length == targets.length;
TypedJumpTableCases typedCases = typedJumpTableCases(jumpTable, caseTokens);
JsonArray mappings = new JsonArray();
JsonArray defaultTargets = new JsonArray();
for (Address target : typedCases.defaults) {
defaultTargets.add(typedJumpTableTarget(target, jumpTable, "Typed default label"));
}
Set<Address> coveredTargets = new HashSet<>(typedCases.cases.values());
coveredTargets.addAll(typedCases.defaults);
if (recoveredAArch64 != null) {
for (int index = 0; index < recoveredAArch64.mappings.size(); index += 1) {
monitor.checkCancelled();
RecoveredAArch64Mapping recovered = recoveredAArch64.mappings.get(index);
if (
typedCases.unknownTargets.contains(recovered.targetAddress) ||
typedCases.ambiguousValues.contains((long) recovered.caseValue)
) continue;
Address typedTarget = typedCases.cases.get((long) recovered.caseValue);
if (typedTarget != null && !typedTarget.equals(recovered.targetAddress)) {
typedCases.cases.remove((long) recovered.caseValue);
typedCases.ambiguousValues.add((long) recovered.caseValue);
typedCases.unknownTargets.add(typedTarget);
typedCases.unknownTargets.add(recovered.targetAddress);
typedCases.limitations.add(
"Typed case evidence and the verified relative table disagree on a destination; both destinations remain unknown for that case."
);
continue;
}
JsonObject mapping = new JsonObject();
mapping.addProperty("case_value", recovered.caseValue);
String targetAddress = canonicalAddress(recovered.targetAddress);
@@ -1566,57 +1606,60 @@ public final class ReaGhidraBridge extends HeadlessScript {
)
);
mapping.add("evidence", evidence);
mappings.add(mapping);
if (!typedCases.cases.containsKey((long) recovered.caseValue)) {
mappings.add(mapping);
}
coveredTargets.add(recovered.targetAddress);
}
}
else {
for (Map.Entry<Long, Address> entry : typedCases.cases.entrySet()) {
JsonObject mapping = typedJumpTableTarget(
entry.getValue(),
jumpTable,
"Typed case value " + entry.getKey()
);
mapping.addProperty("case_value", entry.getKey());
mappings.add(mapping);
}
Set<Address> emittedUnknownTargets = new HashSet<>();
for (int index = 0; index < targets.length; index += 1) {
monitor.checkCancelled();
boolean unresolvedToken = typedCases.unknownTargets.contains(targets[index]);
if (coveredTargets.contains(targets[index]) && !unresolvedToken) continue;
if (!emittedUnknownTargets.add(targets[index])) continue;
JsonObject mapping = new JsonObject();
if (!labelsAligned || labels[index] == null) {
mapping.add("case_value", JsonNull.INSTANCE);
}
else {
mapping.addProperty("case_value", labels[index]);
}
mapping.add("case_value", JsonNull.INSTANCE);
String targetAddress = canonicalAddress(targets[index]);
mapping.addProperty("target_address", targetAddress);
// Ghidra's recovered case-to-target relation does not identify
// which backing load table produced an individual case. Keep the
// table inventory and recovered mappings separate instead of
// attaching every table address to every target.
mapping.addProperty(
"confidence",
labelsAligned && labels[index] != null
? "high"
: "medium"
);
mapping.addProperty("confidence", "medium");
JsonArray evidence = new JsonArray();
evidence.add(
inferenceEvidence(
"jump-table",
"ghidra-high-function",
labelsAligned && labels[index] != null
? "Ghidra paired case value " +
labels[index] +
" with target " +
"Ghidra recovered target " +
targetAddress +
" at dispatch " +
canonicalAddress(jumpTable.getSwitchAddress()) +
"."
: "Ghidra recovered target " +
targetAddress +
" at dispatch " +
canonicalAddress(jumpTable.getSwitchAddress()) +
", but no aligned case value was available."
", but no unambiguous, exactly representable case label or default relationship was available."
)
);
mapping.add("evidence", evidence);
mappings.add(mapping);
}
}
result.add("mappings", mappings);
result.add("default_targets", defaultTargets);
JsonArray limitations = new JsonArray();
for (String limitation : typedCases.limitations) limitations.add(limitation);
if (defaultTargets.isEmpty()) {
limitations.add(
"Ghidra exposed no typed default label with an unambiguous relationship to a recovered destination of this dispatch; default or out-of-range handling remains unknown."
);
}
if (loadTables.length == 0 && referencedDataAddresses.isEmpty()) {
limitations.add(
"Ghidra recovered targets but exposed no backing load-table or dispatch-block data address."
@@ -1631,9 +1674,9 @@ public final class ReaGhidraBridge extends HeadlessScript {
"Load-table metadata was absent; table-level candidate ownership is inferred from typed data references in the dispatch block and its immediate control-flow predecessors, but candidates are not assigned to individual mappings."
);
}
if (recoveredAArch64 == null && !labelsAligned) {
if (!emittedUnknownTargets.isEmpty()) {
limitations.add(
"Case-label and target counts differ or labels are unavailable; all case values are left unknown because index alignment cannot be established."
"Some recovered destinations lack an unambiguous, exactly representable case/default relationship; their case values remain unknown. Unequal label and destination arrays are never paired by index."
);
}
if (recoveredAArch64 == null && loadTables.length == 0 && currentProgram.getLanguageID().getIdAsString().startsWith("AARCH64:")) {
@@ -1648,6 +1691,124 @@ public final class ReaGhidraBridge extends HeadlessScript {
return result;
}
private static final class TypedJumpTableCases {
private final Map<Long, Address> cases = new TreeMap<>();
private final Set<Address> defaults = new java.util.TreeSet<>();
private final Set<Address> unknownTargets = new HashSet<>();
private final Set<Long> ambiguousValues = new HashSet<>();
private final Set<String> limitations = new java.util.LinkedHashSet<>();
}
private TypedJumpTableCases typedJumpTableCases(
JumpTable jumpTable,
List<ClangNode> tokens
) throws Exception {
TypedJumpTableCases result = new TypedJumpTableCases();
Set<Address> targets = new HashSet<>();
for (Address target : jumpTable.getCases()) targets.add(target);
for (ClangNode node : tokens) {
monitor.checkCancelled();
if (!(node instanceof ClangCaseToken token)) continue;
PcodeOp operation = token.getPcodeOp();
if (operation == null || operation.getParent() == null) continue;
PcodeBlockBasic block = operation.getParent();
Address target = block.getStart();
if (!targets.contains(target)) continue;
PcodeOp dispatch = null;
boolean ambiguousDispatch = false;
boolean includesDispatch = false;
for (int index = 0; index < block.getInSize(); index += 1) {
PcodeBlock predecessor = block.getIn(index);
if (!(predecessor instanceof PcodeBlockBasic basic)) continue;
PcodeOp last = basic.getLastOp();
if (last == null || last.getOpcode() != PcodeOp.BRANCHIND) continue;
if (last.getSeqnum().getTarget().equals(jumpTable.getSwitchAddress())) {
includesDispatch = true;
}
if (dispatch != null && dispatch != last) ambiguousDispatch = true;
dispatch = last;
}
if (!includesDispatch) continue;
if (ambiguousDispatch) {
result.unknownTargets.add(target);
result.limitations.add(
"A typed case/default block has multiple indirect dispatch predecessors; REA cannot attribute its label to one switch."
);
continue;
}
// Default is a typed label role, not an OFF sentinel: Ghidra can
// encode a default token with an ordinary in-range case value.
if ("default".equals(token.getText())) {
result.defaults.add(target);
continue;
}
Long value = exactTypedCaseValue(token);
if (value == null) {
result.unknownTargets.add(target);
result.limitations.add(
"Typed case label " + token.getText() +
" has encoded unsigned magnitude " + Long.toUnsignedString(token.getValue()) +
" at recovered target " + canonicalAddress(target) +
" for dispatch " + canonicalAddress(jumpTable.getSwitchAddress()) +
"; the label is nonliteral, inconsistent with its encoded magnitude, or outside the exact JSON integer range, so its numeric case value remains unknown."
);
continue;
}
if (result.ambiguousValues.contains(value)) {
result.unknownTargets.add(target);
continue;
}
Address previous = result.cases.putIfAbsent(value, target);
if (previous != null && !previous.equals(target)) {
result.cases.remove(value);
result.ambiguousValues.add(value);
result.unknownTargets.add(previous);
result.unknownTargets.add(target);
result.limitations.add(
"Typed tokens assign one case value to different recovered destinations; that case remains unknown."
);
}
}
return result;
}
private static Long exactTypedCaseValue(ClangCaseToken token) {
String text = token.getText();
if (text == null || !text.matches("-?(?:0[xX][0-9a-fA-F]+|[0-9]+)")) return null;
boolean negative = text.startsWith("-");
String literal = negative ? text.substring(1) : text;
BigInteger magnitude = literal.startsWith("0x") || literal.startsWith("0X")
? new BigInteger(literal.substring(2), 16)
: new BigInteger(literal, 10);
// Negative numeric tokens carry an unsigned magnitude in OFF, so
// getValue()/getScalar() alone would incorrectly emit positive cases.
if (!magnitude.equals(new BigInteger(Long.toUnsignedString(token.getValue())))) return null;
BigInteger value = negative ? magnitude.negate() : magnitude;
if (value.abs().compareTo(BigInteger.valueOf(9007199254740991L)) > 0) return null;
return value.longValueExact();
}
private static JsonObject typedJumpTableTarget(
Address target,
JumpTable table,
String label
) {
JsonObject result = new JsonObject();
String address = canonicalAddress(target);
result.addProperty("target_address", address);
result.addProperty("confidence", "high");
JsonArray evidence = new JsonArray();
evidence.add(inferenceEvidence(
"jump-table",
"ghidra-clang-case-token",
label + " belongs to the p-code block starting at recovered target " +
address + "; its unique indirect dispatch predecessor is " +
canonicalAddress(table.getSwitchAddress()) + "."
));
result.add("evidence", evidence);
return result;
}
private static final class RecoveredAArch64Mapping {
private final int caseValue;
private final Address targetAddress;
@@ -2351,9 +2512,67 @@ public final class ReaGhidraBridge extends HeadlessScript {
result.addProperty("address", canonicalAddress(function.getEntryPoint()));
result.addProperty("name", procedureName(function));
result.add("classification", functionClassification(function));
result.add("body", functionBody(function));
return result;
}
/** Observe the entire AddressSet; range endpoints are inclusive, not a guessed span. */
private static JsonObject functionBody(Function function) {
JsonArray ranges = new JsonArray();
BigInteger total = BigInteger.ZERO;
Address first = null;
Address last = null;
AddressSpace space = null;
boolean multipleSpaces = false;
AddressRangeIterator iterator = function.getBody().getAddressRanges(true);
while (iterator.hasNext()) {
AddressRange range = iterator.next();
Address start = range.getMinAddress();
Address end = range.getMaxAddress();
if (start.getAddressSpace().getAddressableUnitSize() != 1) {
throw new IllegalStateException("Function body byte coverage requires a byte-addressed space");
}
JsonObject value = new JsonObject();
value.addProperty("start", canonicalAddress(start));
value.addProperty("end", canonicalAddress(end));
ranges.add(value);
BigInteger begin = new BigInteger(Long.toUnsignedString(start.getOffset()));
BigInteger finish = new BigInteger(Long.toUnsignedString(end.getOffset()));
total = total.add(finish.subtract(begin).add(BigInteger.ONE));
if (first == null) {
first = start;
space = start.getAddressSpace();
} else if (!space.equals(start.getAddressSpace())) {
multipleSpaces = true;
}
last = end;
}
JsonObject result = new JsonObject();
result.addProperty("available", true);
result.addProperty("provenance", "ghidra-function-body-address-set");
result.add("ranges", ranges);
result.addProperty("total_bytes", safeBodyByteCount(total));
if (multipleSpaces) {
result.add("span_bytes", JsonNull.INSTANCE);
} else {
BigInteger span = first == null ? BigInteger.ZERO :
new BigInteger(Long.toUnsignedString(last.getOffset()))
.subtract(new BigInteger(Long.toUnsignedString(first.getOffset())))
.add(BigInteger.ONE);
result.addProperty("span_bytes", safeBodyByteCount(span));
}
result.addProperty("non_contiguous", ranges.size() > 1);
result.addProperty("contains_entry", function.getBody().contains(function.getEntryPoint()));
return result;
}
private static long safeBodyByteCount(BigInteger value) {
if (value.signum() < 0 || value.compareTo(BigInteger.valueOf(9007199254740991L)) > 0) {
throw new IllegalStateException("Function body byte coverage exceeds exact JSON integer range");
}
return value.longValueExact();
}
private static String exclusiveEnd(MemoryBlock block) {
Address next = block.getEnd().next();
if (next != null && next.getAddressSpace().equals(block.getStart().getAddressSpace())) {
+25 -2
View File
@@ -241,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",
},
}
@@ -309,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,
}
@@ -553,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,
@@ -764,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:
+10 -9
View File
@@ -26,13 +26,13 @@ func childCount(_ element: AXUIElement) -> Int {
var count = 0
return AXUIElementGetAttributeValueCount(element, kAXChildrenAttribute as CFString, &count) == .success ? count : 0
}
func children(_ element: AXUIElement, limit: Int = 2000) -> [AXUIElement] {
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).map { String($0.prefix(4096)) } ?? NSNull() as Any }
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 }
@@ -108,14 +108,15 @@ func observe(_ request: Request) async throws -> [String: Any] {
var pending: [(AXUIElement, [Int])] = [(root, [])]
while let (element, path) = pending.popLast() {
if nodes.count >= request.max_nodes { truncated = true; break }
let items = children(element)
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": childCount(element)])
if path.count >= 32 { if !items.isEmpty { truncated = true }; continue }
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, items.count)
if childCount(element) > count { truncated = true }
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])) }
}
}
@@ -133,8 +134,8 @@ func observe(_ request: Request) async throws -> [String: Any] {
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: [:]), png.count <= 8 * 1024 * 1024 else {
try fail("image-limit", "Selected-window PNG exceeded 8 MiB")
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]
}
@@ -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
@@ -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; Windows analysis blocked |
| [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 |
+1 -2
View File
@@ -24,7 +24,6 @@ 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"]
end
subgraph "Provider Process Foundation"
@@ -40,7 +39,7 @@ 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"
+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": {
+105
View File
@@ -0,0 +1,105 @@
# DOS MZ analysis with Ghidra
REA can analyze DOS MZ executables 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
```
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.
## 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.
## 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.
+116 -87
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:
@@ -35,7 +40,7 @@ REA supports Node.js 22.19+ and 24.11+ (including newer releases). It uses the n
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,64 @@ 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` |
| 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 +126,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 +147,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 +212,22 @@ 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 or macOS x64/arm64.
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 22 read-only operations: ten inventory/name/search
operations and twelve function-analysis operations. These cover metadata,
decompilation, assembly, resolved calls, typed references, xrefs, function
dossiers, instructions, and recovered data types. GUI controls and annotation
changes require Hopper.
Windows Ghidra operations are currently unavailable. The adapter reports
`unsupported_host` until verified Job Object process ownership, private runtime
DACLs, and reparse-safe path admission are implemented. The
[Windows Ghidra P0 guide](windows-ghidra-p0.md) describes the intended boundary
and remaining controls.
Extract Ghidra and install the JDK outside REA, then export absolute paths:
@@ -212,7 +238,7 @@ rea doctor --json
rea setup
```
PowerShell configuration for Windows uses the same non-secret paths:
For Windows diagnostics, use the same installation paths in PowerShell. These settings do not enable the blocked analysis operations:
```powershell
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
@@ -221,10 +247,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, and agent registration does not enable blocked Ghidra
operations. 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 +265,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, but missing
native ownership and path controls keep Windows operations unavailable.
Operations begin only after default auto-analysis completes. The provider
startup deadline fails the open rather than exposing partial analysis. One
@@ -256,8 +282,9 @@ remain unknown, reference-kind provenance is preserved, and provider-specific
pseudocode is never treated as original source or Hopper-equivalent text.
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 +294,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
+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.
+4 -3
View File
@@ -125,9 +125,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.
+57 -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,7 @@ 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.
+43 -58
View File
@@ -9,15 +9,13 @@ 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
operations callable for the current target, provider, 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`.
optional negotiated client features plus the currently missing features.
Opening or closing a target or observing a provider health transition emits
`notifications/tools/list_changed`.
`binary_session.analysis_provider_candidates` is authoritative for deep-engine
discovery. Target-free discovery is sorted by provider ID, reports host
@@ -57,8 +55,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 +77,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.
+43 -18
View File
@@ -25,7 +25,7 @@ coverage and unknowns. The CLI and MCP use the same application workflows.
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
<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
@@ -96,41 +96,62 @@ 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.
## Approved native desktop observation
## Native desktop observation
`observe_native_ui` captures one explicit existing PID/window ID after
`observation_approved: true`. Screenshots use a selected-window ScreenCaptureKit
`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` additionally requires `actions_approved: true` and
`restore: "leave-as-is"`. Steps select AX child-index paths for press, increment/
`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. The caller must choose whether to recover app state.
post-action capture fails. Application state is left as-is; REA does not attempt
to restore it.
Scenarios allow 16 steps, 30 seconds of total waits, 2,000 AX nodes per capture,
8 MiB PNGs and 64 MiB output within a 180-second deadline. 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. Approved
UI actions may change application data or trigger network activity.
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
Ghidra 12.1.4 with a full 64-bit JDK 21 is bring-your-own. Linux x64 and macOS
x64/arm64 are admitted; macOS requires the matching native decompiler. Windows
x64 P0 remains limited to approved native PE applications. There is no GUI or
mutation authority and no automatic fallback to Hopper.
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. Windows Ghidra analysis is unavailable until
Job Object process ownership, private runtime DACLs, and reparse-safe path checks
are implemented and verified. See [Windows Ghidra P0](windows-ghidra-p0.md) and
[issue #527](https://github.com/morluto/rea/issues/527).
Ghidra has no GUI or mutation 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.
@@ -138,8 +159,12 @@ mutation authority and no automatic fallback to Hopper.
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, selected capture
or a reported OS permission denial, changed-target rejection and cleanup.
- `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
+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.
+41 -33
View File
@@ -1,16 +1,16 @@
{
"package": {
"name": "rea-agents",
"version": "3.2.1",
"version": "4.0.0",
"sdk": {
"server": "2.0.0",
"client": "2.0.0",
"core": "2.0.0"
},
"skill_version": "23"
"skill_version": "24"
},
"tools": {
"total": 122,
"total": 116,
"families": [
{
"id": "direct",
@@ -61,13 +61,12 @@
{
"id": "enhanced",
"surface": "enhanced",
"count": 15,
"count": 14,
"tools": [
"analyze_function",
"analyze_swift_types",
"batch_decompile",
"binary_overview",
"find_code_for_string",
"find_xrefs_to_name",
"get_call_graph",
"get_objc_classes",
@@ -109,14 +108,13 @@
{
"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"
]
@@ -161,16 +159,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"
]
@@ -178,7 +173,7 @@
{
"id": "session",
"surface": "session",
"count": 22,
"count": 21,
"tools": [
"binary_session",
"build_call_path",
@@ -198,7 +193,6 @@
"list_unknowns",
"open_binary",
"record_unknown",
"run_replay_machine",
"update_unknown",
"verify_reconstruction",
"verify_unknown_resolution"
@@ -344,7 +338,6 @@
"capabilities": [
"compare_managed_members",
"import_managed_reconstruction",
"plan_managed_runtime_correlation",
"project_managed_application_graph",
"verify_managed_native_boundaries"
]
@@ -365,9 +358,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"
]
@@ -440,12 +430,36 @@
{
"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": "vscode",
"display_name": "VS Code",
"format": "vscode",
"configuration": "managed"
}
],
"cli": {
"primary_count": 75,
"primary_count": 69,
"commands": [
"analyze",
"analyze-javascript-application",
@@ -473,7 +487,6 @@
"evaluate-reconstruction-coverage",
"evidence-export",
"evidence-import",
"execute-node-characterization",
"extract-artifact",
"function",
"import-managed-reconstruction",
@@ -502,15 +515,10 @@
"observe-javascript-runtime",
"observe-native-ui",
"observe-web-session",
"plan-managed-runtime-correlation",
"policy",
"prepare-node-characterization",
"project-managed-application-graph",
"providers",
"reconcile-javascript-runtime",
"resolve-native-call-targets",
"run-controlled-replay",
"run-replay-machine",
"search",
"setup",
"trace",
@@ -519,7 +527,7 @@
"trace-native-ui-action",
"trace-native-values",
"uninstall",
"upgrade",
"update",
"verify-managed-native-boundaries",
"xrefs"
],
@@ -532,15 +540,15 @@
},
"runtime_catalog": {
"counts": {
"cli_commands": 75,
"mcp_tools": 122,
"cli_commands": 69,
"mcp_tools": 116,
"mcp_prompts": 6
},
"digests": {
"tools_sha256": "1a32398486b1e65f50a1a81de2d8867e6a4c296c165bf9994e53034b466c41b4",
"prompts_sha256": "fa889fc671185ac9c968c6dc8309cf609e709cdc837dbbf39b7cdfadb84ac2d7",
"combined_sha256": "a87e48bc626cb4361f494c074e71852304bfedee17fbba89976d32bb6244f230",
"providers_sha256": "c5a88a49990fafc8244972ee8ff543fc19995eb79db960da55f6a311e15340d1"
"tools_sha256": "7849b6f39bed200cfbf5f4d09ba3f9e0661824efb5753ae2edde8ef2fb66535e",
"prompts_sha256": "4762db348821fec787a5b8c6423f2c48a71f6f0ec2afee97b16ad69030ec504b",
"combined_sha256": "5bb3f994a3c4d0c87a34ff5aab4e28141b5fa6f3a98b5ae7e95686d85d9d983e",
"providers_sha256": "2ae8b85b20be338739e500549d275e78e875aaa10235d2ca283f0ade7787da90"
}
}
}
+48 -51
View File
@@ -1,25 +1,21 @@
# Static-analysis provider evaluation
Status: the Ghidra read-only analysis provider is shipped on Linux x64 and macOS x64/arm64 with matching native tools, and has
an experimental Windows x64 P0 for approved native PE applications. 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 22 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 22 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.
Ghidra does not expose Hopper's GUI or annotation operations.
Windows Ghidra analysis is unavailable, including on a correctly configured
Ghidra/JDK host. Job Object process ownership, private runtime DACLs, and
reparse-safe path checks are not implemented. Package and adapter tests do not
establish real Windows analysis support. 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,
@@ -31,8 +27,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
@@ -53,10 +49,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
@@ -65,18 +61,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 22 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.
@@ -125,24 +122,24 @@ 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 the Windows P0 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` remains blocked by the missing Windows native
authority. After those controls are implemented, the lane must check the
source-owned native x86-64 PE fixture, all read-only operations,
target/snapshot/import SHA-256 linkage, and project, endpoint, process, and
runtime cleanup. The existence of this verifier is not evidence that Windows
Ghidra analysis works.
## 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
@@ -152,12 +149,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 | 22 read-only operations shipped on Linux/macOS with installation checks, temporary projects, a serial API queue, and real ELF/PE/Mach-O conformance. Windows analysis is blocked; GUI and mutation 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.
+66 -30
View File
@@ -1,32 +1,67 @@
# 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, 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 ten inventory operations and twelve function-analysis
operations, for 22 read-only operations total. 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.
Windows Ghidra operations remain unavailable until REA implements verified
Job Object process ownership, private runtime DACLs, and reparse-safe path
admission. Windows package and adapter tests do not establish a usable real
Ghidra session. The [Windows P0 guide](windows-ghidra-p0.md) describes the
intended boundary and remaining requirements.
## Ghidra maintenance boundary
@@ -36,16 +71,17 @@ 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
Before enabling Windows Ghidra operations, 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.
Hopper, and broad filesystem-sensitive workflows remain separate Windows
projects rather than implied 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.
+1 -1
View File
@@ -117,7 +117,7 @@ order rather than sorting or changing timestamps to conceal it.
Malformed archives, broken and cyclic references, unsupported metadata pointers,
relative Objective-C method layouts, ambiguous call candidates, digest mismatch,
cancellation, permission approval, bounded output and owned-resource cleanup
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.
+106 -30
View File
@@ -38,17 +38,22 @@ 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
@@ -95,6 +100,7 @@ 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 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 |
@@ -104,11 +110,49 @@ 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).
## Apple Interface Builder archives
`npm run verify:interface-builder` compiles the source-owned AppKit XIB into a
@@ -122,26 +166,57 @@ 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, without API HTML rendering |
| `npm run docs:api:cached` | Explicit cached API HTML rendering |
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:
```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
@@ -155,8 +230,8 @@ 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. TypeDoc rendering is a separate
command and CI step.
Vitest and Node persistent compile caches are deliberately not enabled by
default. To evaluate repeated local runs, opt in for both cold and warm
@@ -190,8 +265,9 @@ 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
@@ -199,9 +275,9 @@ behavior also requires `npm run verify:package` and the applicable
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 approved actions.
Permission denial fails the positive lane. `npm run verify:native-ui:permissions`
allows a permission-boundary-only result and explicitly reports
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)
+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.
@@ -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_fcfc6a2f890ccb4a5a7d19dce941b8f7644f05babd89e223100ef7b8b7a55afc",
"manifest_id": "ecm_7da021af56cf2eb25489ed7ce494bfe92244e791fa3b5ad5a238a5eb14a23d9a",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
@@ -13,7 +13,7 @@
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "6a884e8e2df17da0df0e8fda01aa3d66726fd7454c5606752c2b3ba3aa9e5449"
"sha256": "411d5b61e237e3b9b2cb9b3a987d83a36134a2ea1810ddd8d9f695dc05709f5b"
}
],
"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": {
+26 -17
View File
@@ -1,23 +1,29 @@
# 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 and currently unavailable. The production adapter reports
`unsupported_host` because verified Job Object process ownership, private
runtime DACLs, and reparse-safe path admission are not implemented. Installation
paths or manual MCP registration do not enable operations.
This guide describes the intended P0 boundary and adapter mechanisms. It does
not establish general Windows REA support. Even after the required controls
are implemented, hostile, sensitive, security-decision, and concurrently
mutable targets require separate acceptance.
## Supported boundary
The P0 accepts exactly:
Once the required native controls are implemented, the intended P0 is limited to:
- a Windows x64 host;
- Node.js 22.19+ or 24.11+;
- 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 22 read-only Ghidra inventory 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
Hopper, Ghidra GUI state, mutation, 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.
@@ -27,7 +33,7 @@ another package on Windows. The production adapter uses the packaged Java
## Configuration
Install REA, Ghidra, and JDK 21 separately. In PowerShell:
For installation diagnostics, configure REA, Ghidra, and JDK 21 in PowerShell. Analysis remains blocked:
```powershell
npm install --global rea-agents
@@ -42,7 +48,9 @@ rea inspect "C:\approved-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.
major version. `rea setup` can configure supported agent integrations and the
bundled REA skill on Windows after approval. It does not install Hopper, Ghidra,
or Java, and configuring an agent does not enable the blocked Ghidra operations.
For an MCP client, register the resolved Node entry point and preserve the
three environment variables above. A representative configuration is:
@@ -135,7 +143,7 @@ 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:
The controlled real-engine lane requires the missing native authority and currently remains blocked:
```powershell
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
@@ -144,13 +152,14 @@ npm ci
npm run verify:ghidra:windows
```
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
The verifier requires the native authority before opening Ghidra. Its intended
checks include a deterministic x86-64 PE fixture, the read-only operation
contract, target/snapshot/import digest linkage, and 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`.
Passing this verifier proves the bounded P0 operation claim. It does not close
the DACL, reparse-point, named-pipe, or Job Object gates above.
Build, package, or adapter-test success does not prove the P0 operation claim.
Real-engine acceptance requires the native controls and the separate
verification lane; broader target and IPC security requirements remain distinct.
+6 -1
View File
@@ -1,6 +1,11 @@
{
"$schema": "./node_modules/knip/schema.json",
"entry": ["src/main.ts", "src/cli.ts", "src/contracts/errorSchemas.ts"],
"entry": [
"src/main.ts",
"src/cli.ts",
"src/contracts/errorSchemas.ts",
"scripts/run-development-tests.mjs"
],
"project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"],
"ignoreDependencies": ["electron"],
"ignoreBinaries": ["ps"],
+407 -19
View File
@@ -1,12 +1,12 @@
{
"name": "rea-agents",
"version": "3.2.1",
"version": "4.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rea-agents",
"version": "3.2.1",
"version": "4.0.0",
"license": "MIT",
"dependencies": {
"@babel/parser": "8.0.4",
@@ -21,12 +21,14 @@
"@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",
"isomorphic-git": "1.38.7",
"jsonc-parser": "3.3.1",
"pino": "9.14.0",
"playwright-core": "1.58.2",
"plist": "5.0.0",
@@ -42,6 +44,7 @@
},
"devDependencies": {
"@fast-check/vitest": "0.4.1",
"@types/cross-spawn": "6.0.6",
"@types/node": "24.10.1",
"@types/semver": "7.7.1",
"@types/write-file-atomic": "4.0.3",
@@ -52,8 +55,8 @@
"jscpd": "5.0.12",
"knip": "6.26.0",
"lint-staged": "16.4.0",
"oxfmt": "0.72.0",
"oxlint": "1.73.0",
"prettier": "3.6.2",
"turbo": "2.10.5",
"typedoc": "0.28.20",
"typescript": "5.9.3",
@@ -1101,6 +1104,329 @@
"win32"
]
},
"node_modules/@oxfmt/binding-android-arm-eabi": {
"version": "0.72.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.72.0.tgz",
"integrity": "sha512-u4U1uaDOVBuorGNC/07NLm48nECHOiJJka002YGML16Md2DRvf38RGBt01lH60EOGvNlFVmbLoksQedNBoZHKw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-android-arm64": {
"version": "0.72.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.72.0.tgz",
"integrity": "sha512-GCnM+Ae2mWOKC+92y3FOb6eZJeVnBAVz82e3lITUUg+kxukYKzBNAvJ/hhHIfa2A0md55SYYhdWPddSSy3KPVA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-darwin-arm64": {
"version": "0.72.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.72.0.tgz",
"integrity": "sha512-LjQ8tlevdtwraFlWC6AnQ1Forzde0clRmnsP3QmK+ydSElDt/J+5Jc+m+//Ly1G97aP7WCNUw110QMIUw2K7xA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-darwin-x64": {
"version": "0.72.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.72.0.tgz",
"integrity": "sha512-+ZbcxhB1C34RQR5IldSyY6Z/yalJIxHXhP36PGE4sEq0f/lgL5jCLmOKoDeVm8dqd9gFGDsd1bNWHSGCAwdSkw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-freebsd-x64": {
"version": "0.72.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.72.0.tgz",
"integrity": "sha512-yGPhA8sePEBFV9iGcprhyOk3Bz3fZshswMH3DpYIPZFLT1DG5LjiZ0j6AJQpSueaL6bUK0av1K+5+fe+ldy5EQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-linux-arm-gnueabihf": {
"version": "0.72.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.72.0.tgz",
"integrity": "sha512-/g5axuqFMcAtoQrFLR5EJEMDjR5Ea4GuzpdDmMpPWAgrF1DmL4kD1NPNVlpXEOxpFJnD6lPvLKYj1lzdlrfifA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-linux-arm-musleabihf": {
"version": "0.72.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.72.0.tgz",
"integrity": "sha512-InVitdHbS09PM+w+WinIiyX37rC9R3L7lUP9mKy9uKTEDSUFaLjWsLoreRIxKivnv7Ndqc6Trvi+K+b6ZzhiJg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-linux-arm64-gnu": {
"version": "0.72.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.72.0.tgz",
"integrity": "sha512-WKjlyRAxpPjXoqeuwFBim5LscgvY+RNEiZ971C9JlRQZONhJTsXDki/oHFYQE+fzzRpzXh3owZvOdT3kNL2g+g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-linux-arm64-musl": {
"version": "0.72.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.72.0.tgz",
"integrity": "sha512-poafnbsPpJhR+5Mbwpf8CscugTyiRAzjTOxRVn8ra3X2jnURs2xL5Du2xl5g2iW85lICMglpmLf8jDjZEbNgzQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-linux-ppc64-gnu": {
"version": "0.72.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.72.0.tgz",
"integrity": "sha512-zNUi0gKRsO7S3IfPzsTbnm88OedPuX32hmZFWZRWHvWJlAoZRwVbeO6Nb3hng4vDgL7qUKp4TGqzPz1mhsycYg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-linux-riscv64-gnu": {
"version": "0.72.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.72.0.tgz",
"integrity": "sha512-qTY7z/iyF/rWI2HWwWMevXqZW5PZUOofUy2afK7v6rseXnpld3yjgyIk6pCux3DqWQ0lB+f5atvLJNBwzLvFsw==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-linux-riscv64-musl": {
"version": "0.72.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.72.0.tgz",
"integrity": "sha512-w18qfo8vEMD6EKEHtzTvjc6jH8yCyXIT1Bn7PFWJd0PpZyzH/VQDx4xvjCh0LGz6GRzqiezbisTdG9AoXtpkZQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-linux-s390x-gnu": {
"version": "0.72.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.72.0.tgz",
"integrity": "sha512-grW8HwEir+vK7fCcq1ZgL3NHQCfZ1PidiERE9Hbf+24xGys+DoDaoqTht4ccfn0PcBwbViLkwkda/KNpJu2KRQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-linux-x64-gnu": {
"version": "0.72.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.72.0.tgz",
"integrity": "sha512-wQJRQfWBRIn88Pkr449EUViwRayrYUScg7ty+Ihiem21mQ9BUA6S9e/4RAgyNk9lMag0KBouyKskjq61rOC0WQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-linux-x64-musl": {
"version": "0.72.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.72.0.tgz",
"integrity": "sha512-T1B0uQzafxuGPjjaPrZJ497nxoi1BiI65JfsYc56nNpmev3msfb/iaTmH2kQuNFqa8PKxpMlf3GhWrWBvBPORQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-openharmony-arm64": {
"version": "0.72.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.72.0.tgz",
"integrity": "sha512-OKxxgTurl7+hypVplO8QRAbbQlwEs0j1Y6FZv+ZN2qNbdxQk2muMlzuJNBGTCWwxQ0rkkOD1uK/DW1fvzoUSJA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-win32-arm64-msvc": {
"version": "0.72.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.72.0.tgz",
"integrity": "sha512-L78TKzURqvjuZOCo88ixMbPXf/RP7u31Y0cmjJ/KEmrHmfYhmhEvsDaTYPiyTRne64jUgCbADfY0YeoQk4/Xyw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-win32-ia32-msvc": {
"version": "0.72.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.72.0.tgz",
"integrity": "sha512-gQtFW+Ii8BWgtJTAD17TY/WsdNS5gUfAPov8iI1jeq4r9lRRYImmxPL08UU00/RLaCfBJwh2UgINnDY0iOiZ+A==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxfmt/binding-win32-x64-msvc": {
"version": "0.72.0",
"resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.72.0.tgz",
"integrity": "sha512-uhooIN+DCzWX5uH7mQkSVCsODqsGluGJ4n3ch4+mITtWKpESMDjUJUSDBAVFuYQDwqY2McPDKdKJbM0oZVW/Qw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxlint/binding-android-arm-eabi": {
"version": "1.73.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.73.0.tgz",
@@ -1894,6 +2220,16 @@
"assertion-error": "^2.0.1"
}
},
"node_modules/@types/cross-spawn": {
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz",
"integrity": "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/deep-eql": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
@@ -3425,6 +3761,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/jsonc-parser": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
"integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
"license": "MIT"
},
"node_modules/knip": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/knip/-/knip-6.26.0.tgz",
@@ -4200,6 +4542,58 @@
"@oxc-resolver/binding-win32-x64-msvc": "11.21.3"
}
},
"node_modules/oxfmt": {
"version": "0.72.0",
"resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.72.0.tgz",
"integrity": "sha512-16OQeL0uhPZbfmT9S9WWM0Nv9+BmVi8Ra1uTYaSKj/qSCs6FB30KOhz9JoNYw6thT+K5bVxsk/tSEOu497i34g==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinypool": "2.2.0"
},
"bin": {
"oxfmt": "bin/oxfmt"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/sponsors/oxc-project"
},
"optionalDependencies": {
"@oxfmt/binding-android-arm-eabi": "0.72.0",
"@oxfmt/binding-android-arm64": "0.72.0",
"@oxfmt/binding-darwin-arm64": "0.72.0",
"@oxfmt/binding-darwin-x64": "0.72.0",
"@oxfmt/binding-freebsd-x64": "0.72.0",
"@oxfmt/binding-linux-arm-gnueabihf": "0.72.0",
"@oxfmt/binding-linux-arm-musleabihf": "0.72.0",
"@oxfmt/binding-linux-arm64-gnu": "0.72.0",
"@oxfmt/binding-linux-arm64-musl": "0.72.0",
"@oxfmt/binding-linux-ppc64-gnu": "0.72.0",
"@oxfmt/binding-linux-riscv64-gnu": "0.72.0",
"@oxfmt/binding-linux-riscv64-musl": "0.72.0",
"@oxfmt/binding-linux-s390x-gnu": "0.72.0",
"@oxfmt/binding-linux-x64-gnu": "0.72.0",
"@oxfmt/binding-linux-x64-musl": "0.72.0",
"@oxfmt/binding-openharmony-arm64": "0.72.0",
"@oxfmt/binding-win32-arm64-msvc": "0.72.0",
"@oxfmt/binding-win32-ia32-msvc": "0.72.0",
"@oxfmt/binding-win32-x64-msvc": "0.72.0"
},
"peerDependencies": {
"svelte": "^5.0.0",
"vite-plus": "*"
},
"peerDependenciesMeta": {
"svelte": {
"optional": true
},
"vite-plus": {
"optional": true
}
}
},
"node_modules/oxlint": {
"version": "1.73.0",
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.73.0.tgz",
@@ -4425,22 +4819,6 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/prettier": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@@ -4963,6 +5341,16 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tinypool": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.2.0.tgz",
"integrity": "sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^20.0.0 || >=22.0.0"
}
},
"node_modules/tinyrainbow": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+25 -22
View File
@@ -1,7 +1,7 @@
{
"name": "rea-agents",
"mcpName": "io.github.morluto/rea",
"version": "3.2.1",
"version": "4.0.0",
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
"license": "MIT",
"repository": {
@@ -61,14 +61,15 @@
"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",
@@ -76,8 +77,8 @@
"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",
@@ -85,20 +86,18 @@
"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",
@@ -110,8 +109,7 @@
"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",
@@ -121,6 +119,8 @@
"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: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",
@@ -131,7 +131,6 @@
"verify:javascript-runtime": "npm run build:cached && node scripts/verify-local-javascript-runtime.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",
@@ -149,21 +148,22 @@
"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",
"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"
"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"
},
"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",
@@ -178,12 +178,14 @@
"@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",
"isomorphic-git": "1.38.7",
"jsonc-parser": "3.3.1",
"pino": "9.14.0",
"playwright-core": "1.58.2",
"plist": "5.0.0",
@@ -195,6 +197,7 @@
},
"devDependencies": {
"@fast-check/vitest": "0.4.1",
"@types/cross-spawn": "6.0.6",
"@types/node": "24.10.1",
"@types/semver": "7.7.1",
"@types/write-file-atomic": "4.0.3",
@@ -205,8 +208,8 @@
"jscpd": "5.0.12",
"knip": "6.26.0",
"lint-staged": "16.4.0",
"oxfmt": "0.72.0",
"oxlint": "1.73.0",
"prettier": "3.6.2",
"turbo": "2.10.5",
"typedoc": "0.28.20",
"typescript": "5.9.3",
+3 -1
View File
@@ -117,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",
});
+4 -4
View File
@@ -1,6 +1,6 @@
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";
@@ -89,7 +89,7 @@ async function sdkToolCatalog() {
}
}
const auxiliaryProviders = [
new ArtifactProvider(false, false, CATALOG_PLATFORM),
new ArtifactProvider(CATALOG_PLATFORM),
new NativeMacOSProvider(undefined, CATALOG_PLATFORM),
new ManagedStaticProvider(),
].map((provider) => ({
@@ -110,7 +110,8 @@ const canonicalizeJson = (value) => {
const payloadJson = JSON.stringify(
canonicalizeJson({ catalog, auxiliaryProviders }),
);
const source = await format(
const source = await formatGeneratedFile(
"src/generatedMcpToolCatalog.ts",
`import type { ToolAnnotations } from "@modelcontextprotocol/server";
import type {
CapabilityDescriptor,
@@ -194,7 +195,6 @@ function isRecord(
return typeof value === "object" && value !== null && !Array.isArray(value);
}
`,
{ parser: "typescript" },
);
await ensureGeneratedFile({
+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
+9 -5
View File
@@ -21,9 +21,6 @@ export async function verifyLargeScreenshotE2e(endpoint, origin) {
PATH: process.env.PATH ?? "/usr/bin:/bin",
REA_LOG_LEVEL: "silent",
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
REA_BROWSER_OBSERVE_ENABLED: "true",
REA_BROWSER_CDP_ENDPOINTS_JSON: JSON.stringify([endpoint]),
REA_BROWSER_ALLOWED_ORIGINS_JSON: JSON.stringify([origin]),
};
let client;
let transport;
@@ -39,7 +36,15 @@ export async function verifyLargeScreenshotE2e(endpoint, origin) {
);
const { stdout } = await promisify(execFile)(
process.execPath,
[entrypoint, "capture-web-screenshot", endpoint, target.id, "--json"],
[
entrypoint,
"capture-web-screenshot",
endpoint,
target.id,
"--allowed-origins",
origin,
"--json",
],
{ env, timeout: 60000, maxBuffer: 72 * 1024 * 1024 },
);
const cliEvidence = parseEvidence(JSON.parse(stdout));
@@ -72,7 +77,6 @@ export async function verifyLargeScreenshotE2e(endpoint, origin) {
name: "capture_web_screenshot",
arguments: {
cdp_endpoint: endpoint,
allowed_origins: [origin],
target_id: target.id,
},
},
+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}`);
+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",
+70 -6
View File
@@ -9,7 +9,7 @@ import { parseEvidence } from "../../dist/domain/evidence.js";
import { analysisProfileSchema } from "../../dist/domain/analysisProfile.js";
import { nativeValueTraceSchema } from "../../dist/domain/nativeValueTrace.js";
const entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
const defaultEntrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
const environment = () => ({
PATH: process.env.PATH ?? "/usr/bin:/bin",
REA_LOG_LEVEL: "silent",
@@ -67,7 +67,12 @@ const verifyGraph = (input, target, procedure, globalAddress) => {
};
/** Verify real Ghidra dependency composition through production CLI and stdio MCP. */
export async function verifyNativeValueE2e(target, procedure, globalAddress) {
export async function verifyNativeValueE2e(
target,
procedure,
globalAddress,
{ entrypoint = defaultEntrypoint } = {},
) {
const { stdout } = await promisify(execFile)(
process.execPath,
[
@@ -142,10 +147,7 @@ export async function verifyNativeValueE2e(target, procedure, globalAddress) {
{ timeout: 180000 },
);
assert.notEqual(result.isError, true, JSON.stringify(result));
const mcpEvidence = parseEvidence({
...result.structuredContent?.evidence,
normalized_result: result.structuredContent?.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);
@@ -157,6 +159,66 @@ export async function verifyNativeValueE2e(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: {},
@@ -175,6 +237,8 @@ export async function verifyNativeValueE2e(target, procedure, globalAddress) {
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,
+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),
);
+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 = [];
+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",
+2 -7
View File
@@ -58,7 +58,6 @@ try {
if (!target.ok) throw target.error;
const observation = await observeNativeUi(target.value, "observe_native_ui", {
...scope,
observation_approved: true,
});
if (
!observation.ok &&
@@ -83,8 +82,6 @@ try {
"capture_native_ui_scenario",
{
...scope,
observation_approved: true,
actions_approved: true,
restore: "leave-as-is",
screenshot: false,
steps: [
@@ -114,8 +111,6 @@ try {
String(scope.pid),
"--window-id",
String(scope.window_id),
"--observation-approved",
"--actions-approved",
"--restore",
"leave-as-is",
"--steps",
@@ -135,14 +130,14 @@ try {
evidence.normalized_result?.steps?.length !== 2
)
throw new Error(
`Native UI CLI did not preserve the approved scenario: ${JSON.stringify(evidence.error ?? evidence.normalized_result?.steps?.map(({ outcome, reason }) => ({ outcome, reason })))}`,
`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, observation_approved: true },
scope,
);
if (mismatch.ok || !mismatch.error.message.includes("target-mismatch"))
throw new Error("Real helper did not reject changed target bytes");
+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 };
}
+12 -1
View File
@@ -1,5 +1,6 @@
import { chmod, mkdir, symlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { supportedClients } from "../dist/application/SupportedClients.js";
/** Create an isolated prefix, fake PATH, client configs, and environment. */
export async function verifyPackageEnvironment({
@@ -48,7 +49,11 @@ export async function verifyPackageEnvironment({
chmod(hopper, 0o755),
chmod(npx, 0o755),
]);
const claudeDir = join(home, "Library/Application Support/Claude");
const claudeDir = supportedClients(home, process.platform, {}).find(
({ name }) => name === "claude_desktop",
)?.markerPath;
if (claudeDir === undefined)
throw new Error("Claude Desktop fixture has no configuration directory");
const codexDir = join(home, ".codex");
const cursorDir = join(home, ".cursor");
await Promise.all([
@@ -69,6 +74,12 @@ export async function verifyPackageEnvironment({
const environment = {
...process.env,
HOME: home,
USERPROFILE: home,
APPDATA: join(home, "AppData", "Roaming"),
CLAUDE_CONFIG_DIR: home,
CODEX_HOME: codexDir,
COPILOT_HOME: join(home, ".copilot"),
OPENCODE_CONFIG: undefined,
XDG_CONFIG_HOME: join(home, ".config"),
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
REA_ANALYSIS_PROVIDER: "auto",
+2 -66
View File
@@ -115,64 +115,6 @@ const verifyManagedReconstruction = async ({
throw new Error("packaged managed reconstruction import CLI failed");
};
const verifyManagedRuntimePlan = async ({
cli,
managedMembers,
managedMethod,
workspace,
environment,
}) => {
const managedRuntimePlanInput = {
static_members: managedMembers,
method: {
token: managedMethod.token,
signature_sha256: managedMethod.signature.raw_sha256,
normalized_il_sha256: managedMethod.body.normalized_il_sha256,
},
requested_effect: "attach",
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,
},
};
const managedRuntimePlan = json(
await run(
cli,
[
"plan-managed-runtime-correlation",
JSON.stringify(managedRuntimePlanInput),
"--json",
],
{
...environment,
REA_MANAGED_RUNTIME_ENABLED: "true",
REA_MANAGED_RUNTIME_ROOTS_JSON: JSON.stringify([workspace]),
REA_MANAGED_RUNTIME_EXECUTABLE_PATH: process.execPath,
},
),
);
if (
managedRuntimePlan.operation !== "plan_managed_runtime_correlation" ||
managedRuntimePlan.provider?.id !== "rea-dotnet-workflows" ||
managedRuntimePlan.normalized_result?.executed !== false ||
managedRuntimePlan.normalized_result?.authority_model?.capability !==
"managed_runtime" ||
managedRuntimePlan.normalized_result?.effect_taxonomy?.attaches_process !==
true ||
managedRuntimePlan.normalized_result?.effect_taxonomy
?.invokes_target_code !== false
)
throw new Error("packaged managed runtime-correlation CLI failed");
};
const verifyManagedBoundaries = async ({ cli, managedPath, environment }) => {
const managedBoundaries = json(
await run(
@@ -332,7 +274,8 @@ const verifyManagedComparison = async ({
if (
managedComparison.operation !== "compare_managed_members" ||
managedComparison.provider?.id !== "rea-dotnet-workflows" ||
managedComparison.normalized_result?.algorithm?.name_matching !== "not-used"
managedComparison.normalized_result?.algorithm?.name_matching !==
"exact-signature-fallback"
)
throw new Error("packaged managed comparison CLI failed");
};
@@ -358,13 +301,6 @@ export async function verifyManaged({ cli, workspace, environment }) {
managedMethod,
environment,
});
await verifyManagedRuntimePlan({
cli,
managedMembers,
managedMethod,
workspace,
environment,
});
const managedBoundaries = await verifyManagedBoundaries({
cli,
managedPath,
+74 -2
View File
@@ -1,3 +1,4 @@
import assert from "node:assert/strict";
import { join } from "node:path";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
@@ -5,6 +6,7 @@ import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { MCP_STARTUP_POLICY } from "../dist/mcpStartupPolicy.js";
import { parseEvidence } from "../dist/domain/evidence.js";
import * as prompts from "./verify-package-prompts.mjs";
import { json, verifyCompleteToolCatalog } from "./lib/verify-package-core.mjs";
@@ -155,8 +157,77 @@ const verifyMcpEvidenceBundle = async (client, mcpOptions, evidenceRoot) => {
throw new Error("packaged MCP evidence import failed");
};
/** Connect to the packaged MCP server and exercise the target-free catalog. */
export async function verifyPackageMcp({ cli, environment, evidenceRoot }) {
const verifyMcpInlineArtifactEvidence = async (
client,
mcpOptions,
artifactArchive,
) => {
const opened = await client.callTool(
{ name: "open_binary", arguments: { path: artifactArchive } },
mcpOptions,
);
assert.notEqual(opened.isError, true, JSON.stringify(opened));
try {
const inspected = await client.callTool(
{ name: "inspect_artifact", arguments: {} },
mcpOptions,
);
assert.notEqual(inspected.isError, true, JSON.stringify(inspected));
const source = parseEvidence(inspected.structuredContent?.evidence);
assert.deepEqual(
source.normalized_result,
inspected.structuredContent?.result,
);
assert.deepEqual(
json(prompts.mcpText(inspected)),
inspected.structuredContent,
);
const compared = await client.callTool(
{
name: "compare_artifacts",
arguments: {
left: inspected.structuredContent.evidence,
right: inspected.structuredContent.evidence,
},
},
mcpOptions,
);
assert.notEqual(compared.isError, true, JSON.stringify(compared));
const comparison = parseEvidence(compared.structuredContent?.evidence);
assert.deepEqual(
comparison.normalized_result,
compared.structuredContent?.result,
);
assert.ok(comparison.evidence_links.includes(source.evidence_id));
const bundle = await client.callTool(
{ name: "get_evidence_bundle", arguments: {} },
mcpOptions,
);
assert.notEqual(bundle.isError, true, JSON.stringify(bundle));
for (const evidence of [source, comparison]) {
assert.deepEqual(
bundle.structuredContent?.result?.records.find(
(record) => record.evidence_id === evidence.evidence_id,
),
evidence,
);
}
} finally {
const closed = await client.callTool(
{ name: "close_binary", arguments: {} },
mcpOptions,
);
assert.notEqual(closed.isError, true, JSON.stringify(closed));
}
};
/** Connect to the packaged MCP server and verify catalog and Evidence composition. */
export async function verifyPackageMcp({
cli,
environment,
evidenceRoot,
artifactArchive,
}) {
const diagnosed = json(
(
await execute(cli, ["mcp", "doctor", "--json"], {
@@ -191,6 +262,7 @@ export async function verifyPackageMcp({ cli, environment, evidenceRoot }) {
await verifyMcpTargetFree(client, mcpOptions);
await verifyMcpUnknownProvider(client, mcpOptions);
await verifyMcpBinaryLifecycle(client, mcpOptions);
await verifyMcpInlineArtifactEvidence(client, mcpOptions, artifactArchive);
await verifyMcpEvidenceBundle(client, mcpOptions, evidenceRoot);
} catch (cause) {
throw new Error(`packaged MCP smoke failed: ${mcpStderr}`, { cause });
+364 -24
View File
@@ -18,6 +18,16 @@ import {
run,
runWithStatus,
} from "./lib/verify-package-core.mjs";
import { parse as parseJsonc } from "jsonc-parser";
const OPENCODE_ORIGINAL = `{
// OpenCode preferences must survive setup.
"model": "provider/model",
"mcp": {
// Keep this independently managed server.
"other": { "type": "local", "command": ["node", "other.js"] },
},
}\n`;
const verifyMcpAdd = async ({ cli, environment, npxLog }) => {
await run(cli, ["mcp", "add"], environment);
@@ -27,12 +37,48 @@ const verifyMcpAdd = async ({ cli, environment, npxLog }) => {
throw new Error("Incur mcp add did not register the pinned npx command");
};
const verifyInteractiveSetup = async ({ command, environment, root }) =>
const snapshotFiles = async (paths) =>
Promise.all(
paths.map(async (path) => {
const exists = await pathExists(path);
return {
path,
exists,
content: exists ? await readFile(path, "utf8") : null,
};
}),
);
const assertFilesUnchanged = async (snapshot, label) => {
for (const entry of snapshot) {
const exists = await pathExists(entry.path);
const content = exists ? await readFile(entry.path, "utf8") : null;
if (exists !== entry.exists || content !== entry.content)
throw new Error(`${label} changed ${entry.path}`);
}
};
const verifyInteractiveSetup = async ({
command,
environment,
root,
unchangedPaths,
}) =>
new Promise((resolvePromise, reject) => {
let output = "";
let cancelled = false;
let settled = false;
const terminal = spawn(command, ["setup"], {
const unchanged = snapshotFiles(unchangedPaths);
const wrapper = `
const { spawnSync } = require("node:child_process");
const child = spawnSync(process.argv[1], ["setup"], { stdio: "inherit", env: process.env, shell: process.platform === "win32" });
const terminal = process.platform === "win32"
? spawnSync("cmd.exe", ["/c", "mode", "CON"], { encoding: "utf8", stdio: ["inherit", "pipe", "inherit"] })
: spawnSync("stty", ["-a"], { encoding: "utf8", stdio: ["inherit", "pipe", "inherit"] });
process.stdout.write("\\n__REA_SETUP_EXIT__" + JSON.stringify({ status: child.status, termios: terminal.stdout, error: child.error?.message }));
process.exit(child.status ?? 1);
`;
const terminal = spawn(process.execPath, ["-e", wrapper, command], {
cwd: root,
env: { ...environment, NO_COLOR: "1", TERM: "xterm-256color" },
name: "xterm-256color",
@@ -55,21 +101,120 @@ const verifyInteractiveSetup = async ({ command, environment, root }) =>
terminal.onData((data) => {
output += data;
if (!cancelled && output.includes("What should REA set up?")) {
if (!cancelled && output.includes("Which agents should use REA?")) {
cancelled = true;
terminal.write("\u0003");
}
});
terminal.onExit(() => {
if (!cancelled || !output.includes("No changes were made.")) {
terminal.onExit(async ({ exitCode }) => {
const marker = output.lastIndexOf("__REA_SETUP_EXIT__");
const result = marker < 0 ? undefined : output.slice(marker + 18).trim();
let childResult;
try {
childResult = result === undefined ? undefined : JSON.parse(result);
} catch {
childResult = undefined;
}
if (
!cancelled ||
exitCode !== 0 ||
childResult?.status !== 0 ||
!output.includes("\u001b[?25h") ||
(process.platform !== "win32" &&
(!childResult.termios?.includes("icanon") ||
!childResult.termios?.includes("echo"))) ||
!output.includes("No changes were made.")
) {
finish(
new Error(
`packaged rea-agents setup did not open and cancel the wizard: ${output}`,
`packaged rea-agents setup did not cancel with a restored terminal and exit 0: ${output}`,
),
);
return;
}
finish();
try {
await assertFilesUnchanged(await unchanged, "Interactive cancellation");
finish();
} catch (error) {
finish(error);
}
});
});
const verifyRedirectedStdoutSetup = async ({
command,
environment,
root,
unchangedPaths,
}) =>
new Promise((resolvePromise, reject) => {
let output = "";
let settled = false;
const unchanged = snapshotFiles(unchangedPaths);
const wrapper = `
const { spawnSync } = require("node:child_process");
const child = spawnSync(process.argv[1], ["setup"], { encoding: "utf8", env: process.env, stdio: ["inherit", "pipe", "inherit"], shell: process.platform === "win32" });
process.stdout.write("\\n__REA_REDIRECTED_SETUP__" + JSON.stringify({ status: child.status, stdout: child.stdout, error: child.error?.message, stdinTTY: process.stdin.isTTY, stderrTTY: process.stderr.isTTY, stdoutRedirected: true }));
process.exit(child.status ?? 1);
`;
const terminal = spawn(process.execPath, ["-e", wrapper, command], {
cwd: root,
env: { ...environment, NO_COLOR: "1", TERM: "xterm-256color" },
name: "xterm-256color",
cols: 120,
rows: 40,
});
const finish = (error) => {
if (settled) return;
settled = true;
clearTimeout(timeout);
if (error === undefined) resolvePromise();
else reject(error);
};
const timeout = setTimeout(() => {
terminal.kill();
finish(
new Error(
`packaged setup started an interactive prompt with redirected stdout: ${output}`,
),
);
}, 15_000);
terminal.onData((data) => {
output += data;
});
terminal.onExit(async ({ exitCode }) => {
const marker = output.lastIndexOf("__REA_REDIRECTED_SETUP__");
let childResult;
try {
childResult =
marker < 0 ? undefined : JSON.parse(output.slice(marker + 24).trim());
} catch {
childResult = undefined;
}
const allOutput = `${output}\n${childResult?.stdout ?? ""}`;
if (
(exitCode !== 0 && exitCode !== 1) ||
childResult?.status !== exitCode ||
childResult.stdinTTY !== true ||
childResult.stderrTTY !== true ||
childResult.stdoutRedirected !== true ||
childResult.stdout?.includes("Which agents should use REA?") ||
allOutput.includes("Which agents should use REA?") ||
/\u001b\[[0-9;?]*[ -/]*[@-~]/u.test(allOutput)
) {
finish(
new Error(
`packaged setup did not remain plain and non-interactive with redirected stdout: ${output}`,
),
);
return;
}
try {
await assertFilesUnchanged(await unchanged, "Redirected-stdout setup");
finish();
} catch (error) {
finish(error);
}
});
});
@@ -79,11 +224,13 @@ const verifySetupPlan = async ({
claudeConfig,
cursorConfig,
codexTarget,
opencodeConfig,
opencodeOriginal,
supportedSetupHost,
}) => {
const plannedExecution = await runWithStatus(
cli,
["setup", "--json"],
["setup", "--all-detected", "--dry-run", "--json"],
environment,
);
const planned = json(plannedExecution.stdout);
@@ -91,15 +238,17 @@ const verifySetupPlan = async ({
const plannedClaudeConfig = await readFile(claudeConfig, "utf8");
const plannedCodexConfig = await readFile(codexTarget, "utf8");
const plannedCursorConfig = await readFile(cursorConfig, "utf8");
const plannedOpenCodeConfig = await readFile(opencodeConfig, "utf8");
if (
planned.status !== "needs_confirmation" ||
plannedExecution.status !== 1 ||
planned.status !== "planned" ||
plannedExecution.status !== 0 ||
planned.appliedActions.length !== 0 ||
!planned.plannedActions.some(({ kind }) => kind === "configure_client") ||
!planned.plannedActions.some(({ kind }) => kind === "install_skill") ||
plannedClaudeConfig !== '{"existing":true}\n' ||
plannedCodexConfig !== 'model = "gpt-5"\n' ||
plannedCursorConfig !== '{"existing":true}\n'
plannedCursorConfig !== '{"existing":true}\n' ||
plannedOpenCodeConfig !== opencodeOriginal
)
throw new Error(
`packaged setup plan was not read-only: ${JSON.stringify({ status: planned.status, exitCode: plannedExecution.status, plannedKinds: planned.plannedActions.map(({ kind }) => kind), appliedKinds: planned.appliedActions.map(({ kind }) => kind), claudeConfig: plannedClaudeConfig, codexConfig: plannedCodexConfig, cursorConfig: plannedCursorConfig })}`,
@@ -108,11 +257,101 @@ const verifySetupPlan = async ({
return { planned };
};
const verifySetupApply = async ({ cli, environment, supportedSetupHost }) => {
const status = process.platform === "linux" ? "needs_human" : "ready";
const verifyAbsentOpenCodeSetup = async ({ cli, environment, home }) => {
const xdgConfigHome = join(home, ".xdg-opencode-absent");
const configPath = join(xdgConfigHome, "opencode", "opencode.json");
if (await pathExists(configPath))
throw new Error("packaged OpenCode first-run fixture was not absent");
const execution = await runWithStatus(
cli,
["setup", "--yes", "--client", "opencode", "--skill=false", "--json"],
{ ...environment, XDG_CONFIG_HOME: xdgConfigHome },
);
const result = json(execution.stdout);
const config = JSON.parse(await readFile(configPath, "utf8"));
if (
execution.status !== 0 ||
result.status !== "ready" ||
!result.appliedActions.includes("configured_opencode") ||
config.mcp?.rea?.type !== "local" ||
config.mcp.rea.enabled !== true ||
JSON.stringify(config.mcp.rea.command) !== JSON.stringify([cli, "mcp"])
)
throw new Error(
`packaged setup did not configure explicitly selected OpenCode without a config file: ${JSON.stringify(result)}`,
);
await rm(xdgConfigHome, { recursive: true, force: true });
};
const verifyAbsentClaudeCodeSetup = async ({ cli, environment, home }) => {
const freshHome = join(home, ".claude-code-absent-home");
const configPath = join(freshHome, ".claude.json");
const markerPath = join(freshHome, ".claude");
await rm(freshHome, { recursive: true, force: true });
await mkdir(freshHome, { recursive: true });
const childEnvironment = {
...environment,
HOME: freshHome,
USERPROFILE: freshHome,
XDG_CONFIG_HOME: join(freshHome, ".config"),
};
delete childEnvironment.CLAUDE_CONFIG_DIR;
if ((await pathExists(configPath)) || (await pathExists(markerPath)))
throw new Error("packaged Claude Code first-run fixture was not absent");
const args = [
"setup",
"--yes",
"--client",
"claude_code",
"--skill=false",
"--json",
];
const firstExecution = await runWithStatus(cli, args, childEnvironment);
const first = json(firstExecution.stdout);
const config = json(await readFile(configPath, "utf8"));
const registration = first.doctor.identity?.registrations?.find(
({ client }) => client === "claude_code",
);
if (
firstExecution.status !== 0 ||
first.status !== "ready" ||
!first.appliedActions.includes("configured_claude_code") ||
config.mcpServers?.rea?.command !== cli ||
JSON.stringify(config.mcpServers?.rea?.args) !== JSON.stringify(["mcp"]) ||
(await pathExists(markerPath)) ||
registration?.state !== "aligned" ||
registration.config_path !== configPath
)
throw new Error(
`packaged setup did not configure an explicitly selected Claude Code with no marker directory: ${JSON.stringify(first)}`,
);
const repeatExecution = await runWithStatus(cli, args, childEnvironment);
const repeat = json(repeatExecution.stdout);
if (
repeatExecution.status !== 0 ||
repeat.status !== "ready" ||
repeat.appliedActions.length !== 0 ||
repeat.plannedActions.length !== 0
)
throw new Error(
`packaged setup was not idempotent for Claude Code without a marker directory: ${JSON.stringify(repeat)}`,
);
await rm(freshHome, { recursive: true, force: true });
};
const verifySetupApply = async ({
cli,
environment,
supportedSetupHost,
hopperSetupSupported,
}) => {
const status = "ready";
const hopperStatus = hopperSetupSupported
? "needs_confirmation"
: "needs_human";
const firstExecution = await runWithStatus(
cli,
["setup", "--yes", "--json"],
["setup", "--yes", "--all-detected", "--json"],
environment,
);
const first = json(firstExecution.stdout);
@@ -124,14 +363,24 @@ const verifySetupApply = async ({ cli, environment, supportedSetupHost }) => {
const aligned = json(alignedExecution.stdout);
const secondExecution = await runWithStatus(
cli,
["setup", "--install-hopper", "--json"],
[
"setup",
"--client",
"claude_desktop",
"--client",
"codex",
"--client",
"cursor",
"--install-hopper",
"--json",
],
environment,
);
const second = json(secondExecution.stdout);
if (supportedSetupHost) {
if (
first.status !== status ||
second.status !== "needs_confirmation" ||
second.status !== hopperStatus ||
firstExecution.status !== (status === "ready" ? 0 : 1) ||
secondExecution.status !== 1 ||
aligned.status !== status ||
@@ -170,6 +419,31 @@ const assertClientConfig = async (cli, configPath) => {
throw new Error("packaged client backup failed");
};
const assertOpenCodeConfig = async ({
cli,
configPath,
expectedRegistration,
}) => {
const contents = await readFile(configPath, "utf8");
const errors = [];
const parsed = parseJsonc(contents, errors, { allowTrailingComma: true });
const config = parsed;
const mcp = config?.mcp;
if (
errors.length !== 0 ||
!contents.includes("// OpenCode preferences must survive setup.") ||
!contents.includes("// Keep this independently managed server.") ||
config?.model !== "provider/model" ||
mcp?.other?.command?.[0] !== "node" ||
(expectedRegistration &&
(mcp?.rea?.type !== "local" ||
mcp.rea.enabled !== true ||
JSON.stringify(mcp.rea.command) !== JSON.stringify([cli, "mcp"]))) ||
(!expectedRegistration && mcp?.rea !== undefined)
)
throw new Error("packaged OpenCode JSONC registration readback failed");
};
const assertCodexSymlink = async ({
cli,
codexConfig,
@@ -245,6 +519,8 @@ const verifyConfigReadback = async ({
root,
skillPath,
siblingSkillPath,
opencodeConfig,
opencodeOriginal,
}) => {
await assertClientConfig(cli, claudeConfig);
await assertClientConfig(cli, cursorConfig);
@@ -255,6 +531,18 @@ const verifyConfigReadback = async ({
codexTarget,
});
await assertSkill({ skillPath, siblingSkillPath, root });
await assertOpenCodeConfig({
cli,
configPath: opencodeConfig,
expectedRegistration: true,
});
if (
(await readFile(`${opencodeConfig}.rea.backup`, "utf8")) !==
opencodeOriginal
)
throw new Error(
"packaged OpenCode backup did not preserve the original JSONC",
);
await assertAlignedDoctor(cli, environment);
};
@@ -269,7 +557,7 @@ const verifySetupFailureRecovery = async ({
await writeFile(cursorConfig, '{"existing":true}\n');
const failedExecution = await runWithStatus(
cli,
["setup", "--yes", "--json"],
["setup", "--yes", "--all-detected", "--json"],
environment,
);
const failed = json(failedExecution.stdout);
@@ -289,7 +577,7 @@ const verifySetupFailureRecovery = async ({
await writeFile(claudeConfig, "{}\n");
const recoveredExecution = await runWithStatus(
cli,
["setup", "--yes", "--json"],
["setup", "--yes", "--all-detected", "--json"],
environment,
);
const recovered = json(recoveredExecution.stdout);
@@ -308,7 +596,7 @@ const verifyDanglingSymlink = async ({ cli, environment, home }) => {
await symlink(join(home, "missing-gemini.json"), geminiConfig);
const danglingExecution = await runWithStatus(
cli,
["setup", "--yes", "--json"],
["setup", "--yes", "--client", "gemini_cli", "--json"],
environment,
);
const dangling = json(danglingExecution.stdout);
@@ -335,7 +623,9 @@ const verifyUninstall = async ({
codexConfig,
cursorTarget,
codexTarget,
opencodeConfig,
}) => {
const openCodeBeforeUninstall = await readFile(opencodeConfig, "utf8");
const uninstallExecution = await runWithStatus(
cli,
["uninstall", "--json"],
@@ -344,6 +634,12 @@ const verifyUninstall = async ({
const uninstall = json(uninstallExecution.stdout);
const cursorAfterUninstall = json(await readFile(cursorTarget, "utf8"));
const codexAfterUninstall = await readFile(codexTarget, "utf8");
const openCodeErrors = [];
const openCodeAfterUninstall = parseJsonc(
await readFile(opencodeConfig, "utf8"),
openCodeErrors,
{ allowTrailingComma: true },
);
if (
uninstall.status !== "complete" ||
uninstallExecution.status !== 0 ||
@@ -357,6 +653,18 @@ const verifyUninstall = async ({
cursorAfterUninstall.mcpServers?.rea !== undefined ||
!codexAfterUninstall.includes('model = "gpt-5"') ||
codexAfterUninstall.includes("mcp_servers.rea") ||
openCodeErrors.length !== 0 ||
openCodeAfterUninstall?.model !== "provider/model" ||
openCodeAfterUninstall?.mcp?.rea !== undefined ||
openCodeAfterUninstall?.mcp?.other?.command?.[0] !== "node" ||
!(await readFile(`${opencodeConfig}.rea.backup`, "utf8")).includes(
"OpenCode preferences must survive setup",
) ||
!(await readFile(`${opencodeConfig}.rea.backup`, "utf8")).includes(
"Keep this independently managed server",
) ||
(await readFile(`${opencodeConfig}.rea.backup`, "utf8")) !==
openCodeBeforeUninstall ||
!(await readFile(`${cursorConfig}.rea.backup`, "utf8")).includes('"rea"') ||
!(await readFile(`${codexConfig}.rea.backup`, "utf8")).includes(
"[mcp_servers.rea]",
@@ -380,13 +688,9 @@ export async function verifyPackageSetup({
codexTarget,
cursorTarget,
supportedSetupHost,
hopperSetupSupported,
root,
}) {
await verifyInteractiveSetup({
command: packageRunnerCli,
environment,
root,
});
await verifyMcpAdd({ cli, environment, npxLog });
const skillPath = join(
home,
@@ -397,20 +701,53 @@ export async function verifyPackageSetup({
await mkdir(join(home, ".agents/skills/unrelated"), { recursive: true });
await writeFile(siblingSkillPath, "unrelated skill\n");
}
const opencodeConfig = join(
environment.XDG_CONFIG_HOME ?? join(home, ".config"),
"opencode",
"opencode.json",
);
if (supportedSetupHost) {
await mkdir(join(opencodeConfig, ".."), { recursive: true });
await writeFile(opencodeConfig, OPENCODE_ORIGINAL);
}
const unchangedPaths = [
claudeConfig,
cursorConfig,
codexTarget,
opencodeConfig,
skillPath,
];
await verifyInteractiveSetup({
command: packageRunnerCli,
environment,
root,
unchangedPaths,
});
await verifyRedirectedStdoutSetup({
command: packageRunnerCli,
environment,
root,
unchangedPaths,
});
await verifySetupPlan({
cli,
environment,
claudeConfig,
cursorConfig,
codexTarget,
opencodeConfig,
opencodeOriginal: OPENCODE_ORIGINAL,
supportedSetupHost,
});
const apply = await verifySetupApply({
cli,
environment,
supportedSetupHost,
hopperSetupSupported,
});
if (supportedSetupHost) {
await verifyAbsentOpenCodeSetup({ cli, environment, home });
await verifyAbsentClaudeCodeSetup({ cli, environment, home });
await verifyConfigReadback({
cli,
environment,
@@ -421,6 +758,8 @@ export async function verifyPackageSetup({
root,
skillPath,
siblingSkillPath,
opencodeConfig,
opencodeOriginal: OPENCODE_ORIGINAL,
});
await verifySetupFailureRecovery({
cli,
@@ -437,6 +776,7 @@ export async function verifyPackageSetup({
codexConfig,
cursorTarget,
codexTarget,
opencodeConfig,
});
} else if (
apply.first.status !== "needs_human" ||
+180
View File
@@ -0,0 +1,180 @@
import { createHash } from "node:crypto";
import { createServer } from "node:http";
import { readFile, writeFile, mkdir } from "node:fs/promises";
import { dirname, join } from "node:path";
import { pathToFileURL } from "node:url";
import { exec, json } from "./lib/verify-package-core.mjs";
/** Exercise the packaged updater with real npm and a local release registry fixture. */
export async function verifyPackageUpdate({
prefix,
packageRoot,
tarball,
workspace,
environment,
}) {
const metadataPath = join(packageRoot, "package.json");
const metadata = json(await readFile(metadataPath, "utf8"));
const archive = await readFile(tarball);
const entry = join(packageRoot, "scripts", "rea.mjs");
const home = join(workspace, "update-home");
const codexPath = join(home, ".codex", "config.toml");
const originalConfig =
'[mcp_servers.rea]\ncommand = "npx"\nargs = ["-y", "rea-agents@0.0.0", "mcp"]\n';
await mkdir(dirname(codexPath), { recursive: true });
await writeFile(codexPath, originalConfig);
const server = createServer((request, response) => {
void serveRegistry(request, response).catch((error) => {
response.writeHead(500);
response.end(String(error));
});
});
let registry;
async function serveRegistry(request, response) {
const pathname = new URL(request.url, registry).pathname;
if (pathname === "/rea-agents") {
response.setHeader("Content-Type", "application/json");
response.end(
JSON.stringify({
name: metadata.name,
"dist-tags": { latest: metadata.version },
versions: {
[metadata.version]: {
...metadata,
dist: {
tarball: `${registry}/release.tgz`,
integrity: `sha512-${createHash("sha512").update(archive).digest("base64")}`,
},
},
},
}),
);
} else if (pathname === "/release.tgz") {
response.end(archive);
} else if (request.method === "POST") {
response.setHeader("Content-Type", "application/json");
response.end("{}");
} else {
const upstream = await fetch(`https://registry.npmjs.org${request.url}`);
response.writeHead(upstream.status, {
"Content-Type":
upstream.headers.get("content-type") ?? "application/octet-stream",
});
response.end(Buffer.from(await upstream.arrayBuffer()));
}
}
await new Promise((resolveListen, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", resolveListen);
});
try {
const address = server.address();
if (typeof address !== "object" || address === null)
throw new Error("Registry fixture did not bind");
registry = `http://127.0.0.1:${address.port}`;
// Model an older installed release's identity while retaining the new updater
// under test. The registry supplies the unmodified packed target artifact.
const generatedPath = join(
packageRoot,
"dist",
"generatedPackageMetadata.js",
);
const generated = await readFile(generatedPath, "utf8");
const previousVersion = "0.0.0";
const previousGenerated = generated.replace(
`version: "${metadata.version}"`,
`version: "${previousVersion}"`,
);
if (previousGenerated === generated)
throw new Error("Could not create previous-release identity fixture");
await writeFile(generatedPath, previousGenerated);
await writeFile(
metadataPath,
JSON.stringify({ ...metadata, version: previousVersion }),
);
const runtimeEnvironment = {
...environment,
HOME: home,
USERPROFILE: home,
APPDATA: join(home, "AppData", "Roaming"),
CLAUDE_CONFIG_DIR: home,
CODEX_HOME: join(home, ".codex"),
COPILOT_HOME: join(home, ".copilot"),
XDG_CONFIG_HOME: join(home, ".config"),
OPENCODE_CONFIG: join(home, ".config", "opencode", "opencode.json"),
npm_config_prefix: prefix,
npm_config_registry: registry,
};
const previous = (
await exec(process.execPath, [entry, "--version"], {
env: runtimeEnvironment,
})
).stdout.trim();
if (previous !== previousVersion)
throw new Error(`Previous-release fixture reported ${previous}`);
const harness = join(workspace, "verify-update.mjs");
await writeFile(
harness,
`
import { runUpdate } from ${JSON.stringify(pathToFileURL(join(packageRoot, "dist/application/Update.js")).href)};
import { systemUpdateHost } from ${JSON.stringify(pathToFileURL(join(packageRoot, "dist/application/UpdateRuntime.js")).href)};
const host = systemUpdateHost(${JSON.stringify(packageRoot)}, ${JSON.stringify(home)});
const result = await runUpdate(${JSON.stringify(previousVersion)}, host, "structured");
process.stdout.write(JSON.stringify(result));
if (result.status === "failed") process.exitCode = 1;
`,
);
const result = json(
(
await exec(process.execPath, [harness], {
env: runtimeEnvironment,
maxBuffer: 16 * 1024 * 1024,
})
).stdout,
);
if (
result.status !== "updated" ||
result.installedVersion !== metadata.version ||
result.command.at(-1) !== `${metadata.name}@${metadata.version}` ||
result.maintenance.status !== "planned" ||
result.maintenance.scope.clients.join(",") !== "codex" ||
result.maintenance.scope.skill !== false
)
throw new Error(
`Packaged updater failed its exact-release contract: ${JSON.stringify(result)}`,
);
if ((await readFile(codexPath, "utf8")) !== originalConfig)
throw new Error(
"Update applied integration maintenance without approval",
);
const current = (
await exec(process.execPath, [entry, "--version"], {
env: runtimeEnvironment,
})
).stdout.trim();
if (current !== metadata.version)
throw new Error(`Updated executable reported ${current}`);
const help = (
await exec(process.execPath, [entry, "--help"], {
env: runtimeEnvironment,
})
).stdout;
if (!/^\s{2}update\s/mu.test(help) || /^\s{2}upgrade\s/mu.test(help))
throw new Error(
"Updated CLI did not expose update as its sole maintenance command",
);
return {
installed_version: result.installedVersion,
exact_release: true,
maintenance_read_only: true,
};
} finally {
server.closeAllConnections();
await new Promise((resolveClose, reject) =>
server.close((error) =>
error === undefined ? resolveClose() : reject(error),
),
);
}
}
+23 -6
View File
@@ -12,6 +12,7 @@ import { verifyPackageDiscovery } from "./verify-package-discovery.mjs";
import { verifyPackageEnvironment } from "./verify-package-environment.mjs";
import { verifyPackageEvidence } from "./verify-package-evidence.mjs";
import { verifyPackageInstall } from "./verify-package-install.mjs";
import { verifyPackageUpdate } from "./verify-package-update.mjs";
import { verifyPackageMcp } from "./verify-package-mcp.mjs";
import { verifyPackagePack } from "./verify-package-pack.mjs";
import { verifyPackagePlatform } from "./verify-package-platform.mjs";
@@ -32,6 +33,7 @@ const referenceRoot = join(workspace, "reference-source");
let tarball;
let mcpStartup;
let mcpModuleLoading;
let update;
try {
({ tarball } = await verifyPackagePack({ root, workspace }));
@@ -48,6 +50,18 @@ try {
workspace,
environment: environmentData.environment,
});
update = await verifyPackageUpdate({
prefix: environmentData.prefix,
packageRoot: join(
environmentData.prefix,
"lib",
"node_modules",
PRODUCT_IDENTITY.packageName,
),
tarball: join(root, tarball),
workspace,
environment: environmentData.environment,
});
mcpStartup = await measureMcpStartup({
command: cli,
args: ["mcp"],
@@ -65,11 +79,12 @@ try {
policy: MCP_STARTUP_POLICY,
packageName: PRODUCT_IDENTITY.packageName,
});
const { supportedSetupHost } = await verifyPackageDiscovery({
cli,
environment: environmentData.environment,
});
await verifyPackageArtifactAndElectron({
const { supportedSetupHost, hopperSetupSupported } =
await verifyPackageDiscovery({
cli,
environment: environmentData.environment,
});
const { artifactArchive } = await verifyPackageArtifactAndElectron({
cli,
workspace,
environment: environmentData.environment,
@@ -109,15 +124,17 @@ try {
codexTarget: environmentData.codexTarget,
cursorTarget: environmentData.cursorTarget,
supportedSetupHost,
hopperSetupSupported,
root,
});
await verifyPackageMcp({
cli,
environment: environmentData.environment,
evidenceRoot,
artifactArchive,
});
process.stdout.write(
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), cli: true, analysisCli: true, artifactCli: true, managedCli: true, managedReconstructionCli: true, managedNativeVerificationCli: true, managedRuntimePlanCli: true, managedApplicationGraphCli: true, evidenceCli: true, incurMcpCommand: PRODUCT_IDENTITY.mcpCommand, lifecycleScriptsRequired: false, doctor: "platform-appropriate", setup: supportedSetupHost ? "planned-then-idempotent" : "unsupported-host-rejected", setupPlanReadOnly: supportedSetupHost, existingHopperPreserved: supportedSetupHost, clients: supportedSetupHost ? 3 : 0, backupReadback: supportedSetupHost, failureRecovery: supportedSetupHost, configSymlinkLifecycle: supportedSetupHost, skill: supportedSetupHost, skillReferences: supportedSetupHost, mcpTools: TOOL_CONTRACTS.length, mcpPrompts: prompts.names.length, promptCompletion: true, promptCompletionLifecycle: true, evidenceMcp: true, targetFree: true, targetLifecycle: true, boundedRegexBridge: true, mcpStartup, mcpModuleLoading })}\n`,
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), cli: true, analysisCli: true, artifactCli: true, managedCli: true, managedReconstructionCli: true, managedNativeVerificationCli: true, managedApplicationGraphCli: true, evidenceCli: true, incurMcpCommand: PRODUCT_IDENTITY.mcpCommand, lifecycleScriptsRequired: false, doctor: "platform-appropriate", setup: supportedSetupHost ? "planned-then-idempotent" : "unsupported-host-rejected", setupPlanReadOnly: supportedSetupHost, existingHopperPreserved: supportedSetupHost, clients: supportedSetupHost ? 4 : 0, backupReadback: supportedSetupHost, failureRecovery: supportedSetupHost, configSymlinkLifecycle: supportedSetupHost, skill: supportedSetupHost, skillReferences: supportedSetupHost, mcpTools: TOOL_CONTRACTS.length, mcpPrompts: prompts.names.length, promptCompletion: true, promptCompletionLifecycle: true, evidenceMcp: true, targetFree: true, targetLifecycle: true, boundedRegexBridge: true, update, mcpStartup, mcpModuleLoading })}\n`,
);
} finally {
if (tarball) await rm(join(root, tarball), { force: true });
+59 -26
View File
@@ -1,10 +1,11 @@
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { execFile } from "node:child_process";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { delimiter, join } from "node:path";
import { promisify } from "node:util";
import { pathToFileURL } from "node:url";
import { lt, rcompare } from "semver";
@@ -44,7 +45,7 @@ transport.stderr?.on("data", (chunk) => {
});
let publishedToolCount = 0;
let upgrade;
let update;
try {
await client.connect(transport);
@@ -71,7 +72,7 @@ try {
});
if (status.isError === true)
throw new Error("Published target-free MCP session check failed");
upgrade = await verifyPublishedUpgrade({ version, canaryRoot });
update = await verifyPublishedUpdate({ version, canaryRoot });
} catch (cause) {
throw new Error(`Published package verification failed: ${stderr}`, {
cause,
@@ -86,17 +87,17 @@ try {
}
process.stdout.write(
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), package: "rea-agents", version, mcpTools: publishedToolCount, canonicalMcpTools: CATALOG_IDENTITY.counts.mcp_tools, upgrade })}\n`,
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), package: "rea-agents", version, mcpTools: publishedToolCount, canonicalMcpTools: CATALOG_IDENTITY.counts.mcp_tools, update })}\n`,
);
async function verifyPublishedUpgrade({ version: targetVersion, canaryRoot }) {
async function verifyPublishedUpdate({ version: targetVersion, canaryRoot }) {
const previousVersion = await findPreviousPublishedVersion(targetVersion);
const prefix = join(canaryRoot, "upgrade-prefix");
const home = join(canaryRoot, "upgrade-home");
const prefix = join(canaryRoot, "update-prefix");
const home = join(canaryRoot, "update-home");
const environment = {
...process.env,
HOME: home,
npm_config_cache: join(canaryRoot, "upgrade-cache"),
npm_config_cache: join(canaryRoot, "update-cache"),
npm_config_prefix: prefix,
PATH: `${join(prefix, "bin")}${delimiter}${process.env.PATH ?? ""}`,
};
@@ -114,33 +115,63 @@ async function verifyPublishedUpgrade({ version: targetVersion, canaryRoot }) {
{ cwd: canaryRoot, env: environment, maxBuffer: 16 * 1024 * 1024 },
);
const { stdout } = await execFileAsync(
"npx",
["rea-agents", "upgrade", "--json"],
{ cwd: canaryRoot, env: environment, maxBuffer: 16 * 1024 * 1024 },
);
const result = JSON.parse(stdout);
if (
result.status !== "upgraded" ||
result.previousVersion !== previousVersion ||
result.latestVersion !== targetVersion
)
throw new Error(
`npx rea-agents upgrade did not upgrade ${previousVersion} to ${targetVersion}: ${stdout}`,
);
const npmRoot = (
await execFileAsync("npm", ["root", "--global"], {
cwd: canaryRoot,
env: environment,
})
).stdout.trim();
// Older releases may predate the update command. Exercise the new published
// updater's production workflow against that real previous installation.
const updaterPrefix = join(canaryRoot, "updater");
await execFileAsync(
"npm",
[
"install",
"--ignore-scripts",
"--no-save",
"--no-package-lock",
"--prefix",
updaterPrefix,
`rea-agents@${targetVersion}`,
],
{ cwd: canaryRoot, env: environment, maxBuffer: 16 * 1024 * 1024 },
);
const updaterRoot = join(updaterPrefix, "node_modules", "rea-agents");
const harness = join(canaryRoot, "update.mjs");
await writeFile(
harness,
`
import { runUpdate } from ${JSON.stringify(pathToFileURL(join(updaterRoot, "dist/application/Update.js")).href)};
import { systemUpdateHost } from ${JSON.stringify(pathToFileURL(join(updaterRoot, "dist/application/UpdateRuntime.js")).href)};
const result = await runUpdate(${JSON.stringify(previousVersion)}, systemUpdateHost(${JSON.stringify(join(npmRoot, "rea-agents"))}, ${JSON.stringify(home)}), "structured");
process.stdout.write(JSON.stringify(result));
if (result.status === "failed") process.exitCode = 1;
`,
);
const { stdout } = await execFileAsync(process.execPath, [harness], {
cwd: canaryRoot,
env: environment,
maxBuffer: 16 * 1024 * 1024,
});
const result = JSON.parse(stdout);
if (
result.status !== "updated" ||
result.previousVersion !== previousVersion ||
result.latestVersion !== targetVersion ||
result.installedVersion !== targetVersion ||
result.command.at(-1) !== `rea-agents@${targetVersion}`
)
throw new Error(
`Published REA updater did not update ${previousVersion} to ${targetVersion}: ${stdout}`,
);
const installed = JSON.parse(
await readFile(join(npmRoot, "rea-agents", "package.json"), "utf8"),
);
if (installed.version !== targetVersion)
throw new Error(
`npx rea-agents upgrade installed ${installed.version}, expected ${targetVersion}`,
`REA update installed ${installed.version}, expected ${targetVersion}`,
);
const help = (
@@ -149,8 +180,10 @@ async function verifyPublishedUpgrade({ version: targetVersion, canaryRoot }) {
env: environment,
})
).stdout;
if (!help.includes("setup"))
throw new Error("the upgraded rea-agents executable did not start");
if (!/^\s{2}update\s/mu.test(help) || /^\s{2}upgrade\s/mu.test(help))
throw new Error(
"the updated executable did not expose only the update command",
);
return {
status: result.status,
+55 -34
View File
@@ -34,15 +34,13 @@ import {
} from "./lib/browser-scenario-verifier.mjs";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
import { verifyLargeScreenshotE2e } from "./lib/browser-screenshot-e2e.mjs";
import { verifyPopupEventCoverage } from "./lib/browser-popup-e2e.mjs";
const SECRET_VALUES = [
"network-secret-value",
"console-secret-value",
"storage-secret-value",
"websocket-secret-value",
"ax-private-label-value",
];
const REAL_BROWSER_STARTUP_TIMEOUT_MS = 30_000;
const SCENARIO_SECRET_VALUE = "rea-browser-verifier-secret";
const SCENARIO_URL_SECRET_VALUE = "rea-browser-url-verifier-secret";
process.env.REA_BROWSER_VERIFIER_SECRET = SCENARIO_SECRET_VALUE;
process.env.REA_BROWSER_VERIFIER_URL = SCENARIO_URL_SECRET_VALUE;
const verifierRun = createVerifierRun();
const executable = await browserExecutable();
@@ -85,7 +83,7 @@ try {
});
const endpoint = `http://127.0.0.1:${String(port)}`;
const provider = new CdpBrowserProvider();
const target = await pageTarget(provider, endpoint, site.origin);
const target = await pageTarget(provider, endpoint);
const observed = await provider.inspectPage(
inspectWebPageInputSchema.parse({
cdp_endpoint: endpoint,
@@ -99,10 +97,25 @@ try {
if (!observed.ok) throw observed.error;
assertObservation(observed.value, site.origin);
const serialized = JSON.stringify(observed.value);
for (const secret of [...SECRET_VALUES, "browser-secret-value"])
if (
!observed.value.target.url.includes("startup=browser-secret-value") ||
!observed.value.network.requests.some((request) =>
request.url.includes("token=network-secret-value"),
)
)
throw new Error(
"Passive browser result did not preserve ordinary URL queries",
);
for (const secret of [
"request-secret-value",
"request-body-secret-value",
"response-secret-value",
"storage-secret-value",
"websocket-secret-value",
])
if (serialized.includes(secret))
throw new Error(
`Passive browser result retained redacted value: ${secret}`,
`Passive browser result included unselected structured data: ${secret}`,
);
const withSource = await provider.inspectPage(
@@ -217,17 +230,12 @@ try {
mode: "connect",
cdp_endpoint: endpoint,
target_id: target,
ownership: "external",
cleanup: "disconnect-only",
},
site.origin,
);
let attachedScenario;
try {
attachedScenario = await runScenarioCli(attachedScenarioInput, {
REA_BROWSER_SCENARIO_CDP_ENDPOINTS_JSON: JSON.stringify([endpoint]),
REA_BROWSER_SCENARIO_ALLOWED_ORIGINS_JSON: JSON.stringify([site.origin]),
});
attachedScenario = await runScenarioCli(attachedScenarioInput);
} catch (cliError) {
const direct =
await new PlaywrightBrowserScenarioProvider().captureScenario(
@@ -239,12 +247,8 @@ try {
underlying instanceof Error
? `${underlying.name}: ${underlying.message}`
: String(underlying ?? direct.error.message);
const redactedDetails = [...SECRET_VALUES, "browser-secret-value"].reduce(
(message, secret) => message.replaceAll(secret, "<redacted>"),
details,
);
throw new Error(
`Attached browser scenario failed through both CLI and provider: ${redactedDetails}`,
`Attached browser scenario failed through both CLI and provider: ${details}`,
{ cause: cliError },
);
}
@@ -273,9 +277,6 @@ try {
{
mode: "launch",
executable_path: executable,
headless: true,
user_data: "temporary-owned",
cleanup: "close-and-delete-profile",
},
site.origin,
),
@@ -290,15 +291,35 @@ try {
if ([...profilesAfter].some((entry) => !profilesBefore.has(entry)))
throw new Error("Scenario launch retained a temporary browser profile");
assertScenarioCapture(launchedScenario.value);
for (const secret of ["network-secret-value", "websocket-url-secret"])
if (
JSON.stringify([attachedScenario, launchedScenario.value]).includes(
secret,
)
)
throw new Error(`Browser scenario retained sensitive value: ${secret}`);
const scenarioResults = JSON.stringify([
attachedScenario,
launchedScenario.value,
]);
for (const retainedValue of [
"token=network-secret-value",
"token=websocket-url-secret",
"authorization=Bearer console-secret-value",
"websocket-secret-value",
])
if (!scenarioResults.includes(retainedValue))
throw new Error(
`Browser scenario did not preserve selected value: ${retainedValue}`,
);
for (const unselectedValue of [
SCENARIO_SECRET_VALUE,
SCENARIO_URL_SECRET_VALUE,
"request-secret-value",
"request-body-secret-value",
"response-secret-value",
"storage-secret-value",
])
if (scenarioResults.includes(unselectedValue))
throw new Error(
`Browser scenario included unselected structured value: ${unselectedValue}`,
);
const largeScreenshot = await verifyLargeScreenshotE2e(endpoint, site.origin);
const popupEvents = await verifyPopupEventCoverage(executable);
process.stdout.write(
`${JSON.stringify({
verifier_run: await completeVerifierRun(verifierRun),
@@ -317,6 +338,7 @@ try {
pageScopedTransport: true,
screenshotBytes: screenshot.value.artifact.bytes,
largeScreenshot,
popupEvents,
browserScenarioCli: true,
browserScenarioAttachCleanup: "disconnected-external",
browserScenarioLaunchCleanup: "terminated-owned-process",
@@ -336,7 +358,7 @@ try {
}
async function verifyPageScopedTransport(provider, proxy, origin) {
const target = await pageTarget(provider, proxy.endpoint, origin);
const target = await pageTarget(provider, proxy.endpoint);
const input = inspectWebPageInputSchema.parse({
cdp_endpoint: proxy.endpoint,
allowed_origins: [origin],
@@ -413,12 +435,11 @@ async function browserExecutable() {
);
}
async function pageTarget(provider, endpoint, origin) {
async function pageTarget(provider, endpoint) {
for (let attempt = 0; attempt < 100; attempt += 1) {
const listed = await provider.listTargets(
listBrowserTargetsInputSchema.parse({
cdp_endpoint: endpoint,
allowed_origins: [origin],
}),
);
if (listed.ok && listed.value.targets[0] !== undefined)
+1 -2
View File
@@ -10,7 +10,7 @@ import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
const executable = process.env.REA_ELECTRON_EXECUTABLE;
if (executable === undefined || executable.length === 0)
throw new Error(
"REA_ELECTRON_EXECUTABLE must be an absolute path to an approved Electron executable",
"REA_ELECTRON_EXECUTABLE must identify the Electron executable for this verification lane",
);
const repositoryRoot = fileURLToPath(new URL("..", import.meta.url));
@@ -46,7 +46,6 @@ const input = electronActiveObservationInputSchema.parse({
window_index: 1,
},
],
approved: true,
});
const verifierRun = createVerifierRun();
const result = await new PlaywrightElectronActiveProvider().capture(input);
+11 -2
View File
@@ -1,3 +1,5 @@
import { hasTypedSwitchEvidence } from "./verify-real-ghidra-switch-assertions.mjs";
import { access } from "node:fs/promises";
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
@@ -107,6 +109,14 @@ export function assertDenseSwitchDossier(dossier, address) {
);
if (
denseTable === undefined ||
denseTable.default_targets.length !== 1 ||
denseTable.default_targets[0].confidence !== "high" ||
!hasTypedSwitchEvidence(
denseTable.default_targets[0].evidence,
"Typed default label",
denseTable.default_targets[0].target_address,
denseTable.dispatch_address,
) ||
denseTable.mappings.some(
({ target_address: target, evidence }) =>
!/^0x[0-9a-f]+$/u.test(target) ||
@@ -146,9 +156,8 @@ function assertNativeApiBoundary(observed, requireJumpTable) {
) &&
mappings.length > 0 &&
mappings.every(
({ target_address: target, data_addresses: addresses, evidence }) =>
({ target_address: target, evidence }) =>
/^0x[0-9a-f]+$/u.test(target) &&
addresses.every((address) => /^0x[0-9a-f]+$/u.test(address)) &&
evidence.some(({ kind }) => kind === "jump-table"),
),
)
+502
View File
@@ -0,0 +1,502 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import {
mkdtemp,
mkdir,
readFile,
readdir,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { isAbsolute, join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseEvidence } from "../dist/domain/evidence.js";
import {
functionBodySchema,
functionDossierSchema,
} from "../dist/domain/hopperValues.js";
import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js";
import { buildDosMzFixture } from "../tests/conformance/ghidra/dos-mz-fixture.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
// This lane intentionally has no game paths, binary seeds or compiler dependency.
if (process.argv.length !== 2)
throw new Error("Usage: node scripts/verify-real-ghidra-dos.mjs");
if (!["linux", "darwin"].includes(process.platform))
throw new Error(
"verify:ghidra:dos supports Linux/macOS hosts only; Windows P0 is PE x86-64 only.",
);
if (process.env.GHIDRA_INSTALL_DIR === undefined)
throw new Error(
"verify:ghidra:dos prerequisite missing: GHIDRA_INSTALL_DIR (bring your own Ghidra).",
);
const installation = inspectGhidraInstallation({
installDir: process.env.GHIDRA_INSTALL_DIR,
...(process.env.JAVA_HOME === undefined
? {}
: { javaHome: process.env.JAVA_HOME }),
});
if (installation.status !== "available")
throw new Error(
`verify:ghidra:dos prerequisite unavailable: ${JSON.stringify(installation)}`,
);
const captureDirectory = process.env.REA_DOS_PROOF_CAPTURE_DIR;
if (captureDirectory !== undefined) {
assert.ok(
isAbsolute(captureDirectory),
"REA_DOS_PROOF_CAPTURE_DIR must be absolute",
);
await mkdir(captureDirectory, { recursive: true, mode: 0o700 });
}
const run = createVerifierRun();
const workspace = await mkdtemp(join(tmpdir(), "rea-dos-proof-"));
const runtime = join(workspace, "runtime");
await mkdir(runtime);
const fixture = buildDosMzFixture();
const targetPath = join(workspace, "fixture.exe");
await writeFile(targetPath, fixture.bytes, { mode: 0o600 });
const sha256 = digest(fixture.bytes);
const entrypoint = fileURLToPath(new URL("./rea.mjs", import.meta.url));
const env = {
PATH: process.env.PATH ?? "/usr/bin:/bin",
TMPDIR: runtime,
REA_LOG_LEVEL: "silent",
REA_ANALYSIS_PROVIDER: "ghidra",
REA_PROCESS_RUN_ID: run.run_id,
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
GHIDRA_INSTALL_DIR: process.env.GHIDRA_INSTALL_DIR,
...(process.env.JAVA_HOME === undefined
? {}
: { JAVA_HOME: process.env.JAVA_HOME }),
};
const address = (offset) =>
`0x${(fixture.load_segment * 16 + offset).toString(16)}`;
const expected = {
entry: address(fixture.entry_offset),
near: address(fixture.near_offset),
far: address(fixture.far_offset),
};
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
const client = new Client({ name: "rea-real-dos-proof", version: "1" });
let opened = false;
let stderr = "";
let report;
try {
await client.connect(transport);
transport.stderr?.on("data", (chunk) => {
stderr = (stderr + chunk.toString()).slice(-65536);
});
const target = await call("open_binary", {
path: targetPath,
provider: "ghidra",
});
opened = true;
assert.equal(target.format, "dos-mz");
assert.equal(target.kind, "executable");
assert.equal(target.architecture, "x86");
assert.equal(target.sha256, sha256);
const procedures = await call("list_procedures");
assert.ok(
Array.isArray(procedures),
"Public procedure inventory is not an array",
);
const identities = Object.fromEntries(
Object.entries(expected).map(([key, value]) => {
const item = procedures.find((candidate) => candidate.address === value);
assert.ok(
item,
`Source-owned ${key} procedure ${value} was not discovered from MZ entry/call flow`,
);
return [key, item];
}),
);
const session = await call("binary_session");
const profile = session.analysis_provider_binding?.analysis_profile;
assert.ok(profile, "Public session omitted committed profile");
assertProfile(profile);
assert.equal(session.sha256, sha256);
assert.equal(session.open, true);
const facts = {};
for (const [key, identity] of Object.entries(identities)) {
const procedure = identity.address; // Use returned addresses, never symbol-name heuristics.
assert.match(procedure, /^0x[0-9a-f]+$/u);
const resolved = await call("procedure_address", { procedure });
assert.equal(resolved, procedure, "Flat address round-trip drifted");
const info = await call("procedure_info", { procedure });
const dossierResult = await evidenceCall("analyze_function", { procedure });
const dossier = functionDossierSchema.parse(
dossierResult.normalized_result,
);
assertBody(info.body, procedure);
assertBody(dossier.procedure.body, procedure);
assert.equal(info.entrypoint, procedure);
assert.equal(info.length, info.body.total_bytes);
assert.deepEqual(dossier.procedure.body, info.body);
const containingQueries = [procedure, info.body.ranges.at(-1).end];
for (const query of containingQueries) {
const containing = await call("resolve_containing_procedure", {
address: query,
});
assert.equal(containing.found, true);
assert.equal(containing.query_address, query);
assert.equal(containing.procedure.address, procedure);
assert.deepEqual(containing.procedure.body, info.body);
}
if (key === "entry") {
const gap = await call("resolve_containing_procedure", {
address: address(0x10),
});
assert.equal(
gap.found,
false,
"Gap inside enclosing span was treated as function body",
);
assert.equal(gap.reason, "not_in_procedure");
}
assert.ok(
dossier.pseudocode.trim().length > 0,
"Real DOS decompilation returned empty pseudocode",
);
assert.equal(dossier.procedure.address, procedure);
const window = await call("read_function_instructions", { procedure });
assert.deepEqual(window.procedure.body, info.body);
assert.ok(window.instructions.length > 0);
if (key === "entry") {
assert.deepEqual(info.body.ranges, [
{ start: procedure, end: address(fixture.entry_first_bytes - 1) },
{
start: address(fixture.entry_tail_offset),
end: address(
fixture.entry_tail_offset + fixture.entry_tail_bytes - 1,
),
},
]);
assert.equal(
info.body.total_bytes,
fixture.entry_first_bytes + fixture.entry_tail_bytes,
);
assert.equal(
info.body.span_bytes,
fixture.entry_tail_offset + fixture.entry_tail_bytes,
);
assert.equal(info.body.non_contiguous, true);
} else {
const size =
key === "near" ? fixture.near_body_bytes : fixture.far_body_bytes;
assert.deepEqual(info.body.ranges, [
{
start: procedure,
end: address(
(key === "near" ? fixture.near_offset : fixture.far_offset) +
size -
1,
),
},
]);
assert.equal(info.body.total_bytes, size);
assert.equal(info.body.span_bytes, size);
}
const cli = await cliEvidence("function", procedure);
if (captureDirectory !== undefined) {
await writeFile(
join(captureDirectory, `public-fixture-${key}-cli.json`),
`${JSON.stringify(cli, null, 2)}\n`,
{ mode: 0o600 },
);
await writeFile(
join(captureDirectory, `public-fixture-${key}-mcp.json`),
`${JSON.stringify(dossierResult, null, 2)}\n`,
{ mode: 0o600 },
);
}
const cliDossier = functionDossierSchema.parse(cli.normalized_result);
assert.deepEqual(
stableDossierObservations(cliDossier),
stableDossierObservations(dossier),
"CLI/MCP stable DOS dossier observations differ",
);
facts[key] = {
address: procedure,
body: info.body,
instruction_count: window.instructions.length,
pseudocode_nonempty: true,
pseudocode: dossier.pseudocode,
cli_mcp_observations_equal: true,
containing_procedure_body_equal: true,
limitations: dossier.limitations,
};
}
const move = await call("inspect_native_instruction", {
address: identities.entry.address,
});
assert.equal(move.status, "decoded");
assert.equal(move.length, 3);
assert.equal(hexBytes(move.bytes), "b83412");
assert.match(move.mnemonic, /^MOV$/iu);
assert.ok(
move.operands
.flatMap((operand) => operand.components)
.some(
(part) =>
part.kind === "register" &&
/^AX$/iu.test(part.text) &&
part.bit_width === 16,
),
"DOS entry was not decoded as 16-bit AX",
);
const nearMemory = await call("inspect_native_instruction", {
address: identities.near.address,
});
assert.equal(nearMemory.status, "decoded");
assert.equal(nearMemory.length, 2);
assert.equal(hexBytes(nearMemory.bytes), "fe00");
assert.match(nearMemory.mnemonic, /^INC$/iu);
const nearCall = await call("inspect_native_instruction", {
address: address(fixture.near_call_offset),
});
const farCall = await call("inspect_native_instruction", {
address: address(fixture.far_call_offset),
});
assert.equal(hexBytes(nearCall.bytes), "e81a00");
// Source far segment 0004 is relocated by the independently fixed 1000 load segment.
assert.equal(hexBytes(farCall.bytes), "9a00000410");
for (const [key, instruction] of [
["near", nearCall],
["far", farCall],
]) {
assert.equal(instruction.flow.kind, "call");
assert.ok(
instruction.flow.direct_destinations.includes(identities[key].address),
);
const targets = await call("resolve_native_call_targets", {
address: instruction.address,
});
assert.equal(targets.status, "direct");
assert.ok(
targets.targets.some(
(target) => target.address === identities[key].address,
),
);
}
const invalid = await client.callTool({
name: "procedure_info",
arguments: { procedure: "0xffffffffffffffff" },
});
assert.equal(invalid.isError, true, "Unmapped address did not fail");
const cliWindow = await cliEvidence("instructions", identities.entry.address);
assert.deepEqual(
cliWindow.normalized_result,
await call("read_function_instructions", {
procedure: identities.entry.address,
}),
);
assert.equal(
digest(await readFile(targetPath)),
sha256,
"Analysis changed original bytes",
);
await call("close_binary");
opened = false;
assert.equal((await call("binary_session")).open, false);
report = {
ok: true,
mocked: false,
lane: "ghidra-dos",
host: process.platform,
provider: { id: "ghidra", version: installation.providerVersion },
target: {
format: "dos-mz",
architecture: "x86",
sha256,
bytes: fixture.bytes.length,
},
profile: {
digest: profile.digest,
loader: profile.parameters.loader,
language_id: profile.parameters.language_id,
compiler_spec_id: profile.parameters.compiler_spec_id,
load_segment: profile.parameters.load_segment,
},
transport: { cli: true, stdio_mcp: true },
functions: facts,
instruction_oracles: {
entry_bytes: hexBytes(move.bytes),
near_memory_bytes: hexBytes(nearMemory.bytes),
near_call_bytes: hexBytes(nearCall.bytes),
relocated_far_call_bytes: hexBytes(farCall.bytes),
near_target: expected.near,
far_target: expected.far,
},
invalid_address_rejected: true,
original_unchanged: true,
limitations: [
"Read-only import/decompilation evidence, not DOS runtime execution or whole-program semantic equivalence.",
"The complete body is Ghidra's observed AddressSet, not independently proven historical compiler ownership.",
"Language/compiler identity is bound by the production authenticated bridge handshake; profile loader is the declared import policy.",
"Native high-pcode flow is excluded from cross-runtime parity: LOAD/STORE address-space selector constants are process-specific in current Ghidra output. Both dossiers are still strictly parsed.",
"Windows P0 remains unsupported for DOS MZ.",
],
};
} catch (error) {
if (stderr.trim())
process.stderr.write(`DOS verifier MCP diagnostics:\n${stderr}\n`);
throw error;
} finally {
try {
if (opened) {
try {
await call("close_binary");
} catch (error) {
process.stderr.write(`DOS cleanup close failed: ${String(error)}\n`);
}
}
try {
await client.close();
} finally {
await transport.close();
}
assert.equal(
digest(await readFile(targetPath)),
sha256,
"Original changed during cleanup",
);
const leftovers = (await readdir(runtime)).filter((name) =>
name.startsWith("rea-ghidra-"),
);
assert.deepEqual(leftovers, [], "Owned Ghidra runtime survived close");
} finally {
await rm(workspace, { recursive: true, force: true });
}
}
assert.ok(report, "No DOS proof report was produced");
const verifierRun = await completeVerifierRun(run);
if (verifierRun.process_lineage.status === "verified")
assert.deepEqual(
verifierRun.process_lineage.descendants,
[],
"Owned descendants survived verifier cleanup",
);
process.stdout.write(
`${JSON.stringify({ verifier_run: verifierRun, ...report, cleanup: "complete" })}\n`,
);
function digest(bytes) {
return createHash("sha256").update(bytes).digest("hex");
}
function hexBytes(bytes) {
assert.equal(typeof bytes, "string");
return bytes.replace(/\s/gu, "").toLowerCase();
}
function assertProfile(profile) {
assert.equal(
profile.parameters.function_body_evidence,
"complete-inclusive-ranges-v1",
);
assert.equal(profile.provider.id, "ghidra");
assert.equal(profile.parameters.target_format, "dos-mz");
assert.equal(profile.parameters.import_mode, "ephemeral-read-only");
assert.equal(profile.parameters.loader, "MzLoader");
assert.equal(profile.parameters.language_id, "x86:LE:16:Real Mode");
assert.equal(profile.parameters.compiler_spec_id, "default");
assert.equal(profile.parameters.load_segment, "0x1000");
}
function assertBody(input, entry) {
const body = functionBodySchema.parse(input);
assert.equal(body.available, true);
assert.equal(body.provenance, "ghidra-function-body-address-set");
assert.equal(body.contains_entry, true);
let total = 0n;
let previousEnd = null;
for (const range of body.ranges) {
assert.match(range.start, /^0x[0-9a-f]+$/u);
assert.match(range.end, /^0x[0-9a-f]+$/u);
const start = BigInt(range.start),
end = BigInt(range.end);
assert.ok(start <= end);
if (previousEnd !== null) assert.ok(start > previousEnd + 1n);
previousEnd = end;
total += end - start + 1n;
}
assert.ok(
body.ranges.some(
(range) =>
BigInt(range.start) <= BigInt(entry) &&
BigInt(entry) <= BigInt(range.end),
),
);
assert.equal(BigInt(body.total_bytes), total);
assert.equal(body.non_contiguous, body.ranges.length > 1);
assert.equal(
BigInt(body.span_bytes),
BigInt(body.ranges.at(-1).end) - BigInt(body.ranges[0].start) + 1n,
);
}
async function call(name, arguments_ = {}) {
const result = await client.callTool(
{ name, arguments: arguments_ },
{ timeout: 240000 },
);
assert.notEqual(result.isError, true, JSON.stringify(result));
assert.ok(
result.structuredContent && "result" in result.structuredContent,
`${name} omitted structured result`,
);
return result.structuredContent.result;
}
async function evidenceCall(name, arguments_) {
const result = await client.callTool(
{ name, arguments: arguments_ },
{ timeout: 240000 },
);
assert.notEqual(result.isError, true, JSON.stringify(result));
const evidence = parseEvidence({
...result.structuredContent?.evidence,
normalized_result: result.structuredContent?.result,
});
assert.equal(evidence.subject.digest.sha256, sha256);
assert.equal(evidence.provider.id, "rea-workflow");
assert.equal(evidence.analysis_profile.provider.id, "rea-workflow");
assertProfile(evidence.analysis_profile.parameters.upstream_analysis_profile);
return evidence;
}
async function cliEvidence(command, procedure) {
const { stdout } = await promisify(execFile)(
process.execPath,
[
entrypoint,
command,
targetPath,
procedure,
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
);
const evidence = parseEvidence(JSON.parse(stdout));
assert.equal(evidence.subject.digest.sha256, sha256);
assert.equal(evidence.provider.id, "ghidra");
assertProfile(evidence.analysis_profile);
assert.equal(digest(await readFile(targetPath)), sha256);
return evidence;
}
// Preserve all body/instruction/decompilation/reference observations; raw high-pcode
// address-space selector tokens are process-specific and are not a semantic oracle.
function stableDossierObservations(dossier) {
const { native_value_flow: flow, ...observations } = dossier;
assert.ok(flow, "Dossier omitted native value-flow availability");
return observations;
}
+34
View File
@@ -613,3 +613,37 @@ export async function verifyNativeValueTrace(client, procedures, target) {
const global = requireProcedure(names, "rea_ghidra_inventory_global");
return verifyNativeValueE2e(target, entry.address, global.address);
}
/** Match the source default's -1 assignment in supported native compiler encodings. */
export async function verifyDenseDefaultReturn(client, targetAddress) {
const instruction = await functionCall(client, "inspect_native_instruction", {
address: targetAddress,
});
if (
instruction.status !== "decoded" ||
!isDefaultMinusOneAssignment(instruction)
)
throw new Error(
`Dense default target lacks the source -1 assignment (x86 register/stack MOV or AArch64 W-register MOVN): ${JSON.stringify(instruction)}`,
);
return {
target_address: targetAddress,
instruction_bytes: instruction.bytes,
};
}
function isDefaultMinusOneAssignment(instruction) {
if (instruction.architecture.startsWith("x86:"))
return (
instruction.bytes === "b8ffffffff" ||
/^c745[0-9a-f]{2}ffffffff$/u.test(instruction.bytes)
);
if (
instruction.architecture.startsWith("AARCH64:") &&
instruction.bytes.length === 8
) {
const word = Buffer.from(instruction.bytes, "hex").readUInt32LE();
return (word & 0xffffffe0) === 0x12800000;
}
return false;
}
@@ -0,0 +1,420 @@
import assert from "node:assert/strict";
/** Verify numeric/default evidence against independent compiler-byte/source oracles. */
export function assertSwitchBoundary(boundary, fixture) {
assert.equal(boundary.available, true);
assert.equal(boundary.provenance, "ghidra-high-function");
assert.ok(
boundary.limitations.length > 0,
"Provider observation scope must remain explicit",
);
if (!fixture.table) {
assert.deepEqual(
boundary.jump_tables,
[],
"Comparison-only control fabricated a jump table",
);
return { numeric_cases: 0, default_targets: 0, unknowns: 0 };
}
assert.equal(
boundary.jump_tables.length,
1,
`Expected one source-owned switch: ${fixture.name}`,
);
const table = boundary.jump_tables[0];
if (fixture.allow_unsigned_32_labels === true) {
assert.equal(fixture.discriminator_size_bytes, 4);
assert.equal(boundary.parameters.length, 1);
assert.equal(boundary.parameters[0].data_type, "undefined4");
assert.equal(boundary.parameters[0].size_bytes, 4);
assert.equal(boundary.parameters[0].confidence, "low");
}
assert.equal(
table.dispatch_address,
fixture.dispatch_address,
"Dispatch does not identify the independently byte-proven indirect jump",
);
const numericCount = assertCaseMappings(table, fixture);
assert.equal(
table.default_targets.length,
1,
"Default target must be separate from numeric cases/unknowns",
);
const defaultTarget = table.default_targets[0];
assert.equal(
defaultTarget.target_address,
fixture.default_address,
"Default does not enter the independently byte-proven default block",
);
assert.equal(defaultTarget.confidence, "high");
assert.ok(
hasTypedSwitchEvidence(
defaultTarget.evidence,
"Typed default label",
defaultTarget.target_address,
table.dispatch_address,
),
"Default omitted typed label/block/dispatch evidence",
);
assert.ok(
table.data_sources.some(
({ address, entry_size_bytes: size, entry_count: count }) =>
address === fixture.table_address &&
size === 8 &&
count === fixture.slots.length,
),
"Recovered backing-table observation disagrees with independently decoded bytes",
);
return {
numeric_cases: numericCount,
default_targets: 1,
unknowns: fixture.unsafe_integer ? table.mappings.length : 0,
};
}
function assertCaseMappings(table, fixture) {
const explicit = fixture.slots.filter(({ explicit_case }) => explicit_case);
const numeric = table.mappings.filter(
({ case_value }) => typeof case_value === "number",
);
if (fixture.unsafe_integer) {
assert.equal(
numeric.length,
0,
"Unsafe integer labels were rounded, truncated or replaced by table indices",
);
assert.equal(
table.mappings.length,
fixture.blocks.length,
"Unsafe labels must retain every independently byte-proven target",
);
assert.deepEqual(
table.mappings.map(({ target_address }) => target_address).sort(),
fixture.blocks.map(({ address }) => address).sort(),
);
for (const mapping of table.mappings) {
assert.equal(mapping.case_value, null);
assert.ok(
["low", "medium"].includes(mapping.confidence),
"Unknown numeric association must not claim high confidence",
);
assertUnknownTargetEvidence(
mapping.evidence,
mapping.target_address,
table.dispatch_address,
);
}
} else {
assert.equal(
table.mappings.length,
numeric.length,
"Complete public fixture has unresolved/misclassified numeric case rows",
);
const matchedValues = new Set();
for (const mapping of numeric) {
const expected = fixture.slots.find(
({ value }) =>
value === mapping.case_value ||
(fixture.allow_unsigned_32_labels === true &&
Number(BigInt.asUintN(32, BigInt(value))) === mapping.case_value),
);
assert.ok(expected, `Fabricated out-of-range case ${mapping.case_value}`);
assert.ok(
!matchedValues.has(expected.value),
"Duplicate discriminator bit pattern",
);
matchedValues.add(expected.value);
assert.equal(
mapping.target_address,
expected.target_address,
`Wrong case→target ${fixture.name}:${mapping.case_value}`,
);
assert.equal(mapping.confidence, "high");
assert.ok(
hasTypedSwitchEvidence(
mapping.evidence,
`Typed case value ${mapping.case_value}`,
mapping.target_address,
table.dispatch_address,
),
"Numeric mapping omitted typed case/block/dispatch evidence",
);
}
for (const { value } of explicit)
assert.ok(
matchedValues.has(value),
`Omitted source case ${fixture.name}:${value}`,
);
}
return numeric.length;
}
/** Match typed labels to their explicitly evidenced block and unique dispatch. */
export function hasTypedSwitchEvidence(evidence, label, target, dispatch) {
return evidence.some(
({ kind, source, detail }) =>
kind === "jump-table" &&
source === "ghidra-clang-case-token" &&
typeof detail === "string" &&
detail.trim().length > 0 &&
detail.startsWith(
`${label} belongs to the p-code block starting at recovered target ${target};`,
) &&
detail.includes(
`its unique indirect dispatch predecessor is ${dispatch}.`,
),
);
}
function assertUnknownTargetEvidence(evidence, target, dispatch) {
assert.ok(
evidence.some(
({ kind, source, detail }) =>
kind === "jump-table" &&
source === "ghidra-high-function" &&
typeof detail === "string" &&
detail.trim().length > 0 &&
detail.startsWith(
`Ghidra recovered target ${target} at dispatch ${dispatch},`,
) &&
detail.includes(
"no unambiguous, exactly representable case label or default relationship",
),
),
"Unknown mapping omitted recovered target/dispatch and unavailable label evidence",
);
}
/** Fault-injected guard controls only; these records are not real provider acceptance. */
export function assertSwitchOracleControls(fixtures) {
const fixture = fixtures.find(({ name }) => name === "holes_shared");
assert.ok(fixture);
const boundary = guardBoundary(fixture);
const evidence = boundary.jump_tables[0].default_targets[0].evidence;
assertSwitchBoundary(boundary, fixture);
const faults = {
consistently_wrong_dispatch: (table) => {
const previous = table.dispatch_address;
table.dispatch_address = "0xff";
for (const row of [...table.mappings, ...table.default_targets])
for (const evidence of row.evidence)
evidence.detail = evidence.detail.replace(previous, "0xff");
},
wrong_numeric_evidence_source: (table) => {
table.mappings[0].evidence[0].source = "unrelated-source";
},
wrong_default_evidence_source: (table) => {
table.default_targets[0].evidence[0].source = "ghidra-high-function";
},
empty_default_evidence_detail: (table) => {
table.default_targets[0].evidence[0].detail = "";
},
wrong_typed_dispatch: (table) => {
table.mappings[0].evidence[0].detail =
table.mappings[0].evidence[0].detail.replace(
`predecessor is ${fixture.dispatch_address}.`,
"predecessor is 0xff.",
);
},
missing_default: (table) => {
table.default_targets = [];
},
default_misclassified_as_unknown: (table) => {
table.default_targets = [];
table.mappings.push({
case_value: null,
target_address: fixture.default_address,
confidence: "low",
evidence,
});
},
wrong_shared_target: (table) => {
table.mappings.find(({ case_value }) => case_value === 3).target_address =
fixture.blocks[0].address;
},
positional_label_guess: (table) => {
table.mappings.find(({ case_value }) => case_value === 9).case_value = 99;
},
omitted_case: (table) => {
table.mappings = table.mappings.filter(
({ case_value }) => case_value !== 2,
);
},
wrong_default_target: (table) => {
table.default_targets[0].target_address = fixture.blocks[0].address;
},
wrong_backing_extent: (table) => {
table.data_sources[0].entry_count--;
},
};
for (const [name, inject] of Object.entries(faults)) {
const damaged = structuredClone(boundary);
inject(damaged.jump_tables[0]);
assert.throws(
() => assertSwitchBoundary(damaged, fixture),
undefined,
`Oracle failed to reject ${name}`,
);
}
const negative = fixtures.find(({ name }) => name === "negative");
assert.ok(negative);
const signed = guardBoundary(negative);
assertSwitchBoundary(signed, negative);
signed.jump_tables[0].mappings = signed.jump_tables[0].mappings.map(
(mapping) => ({ ...mapping, case_value: Math.abs(mapping.case_value) }),
);
assert.throws(
() => assertSwitchBoundary(signed, negative),
undefined,
"Lost unary minus in a signed case label",
);
const unsignedControls = assertStrippedSignednessControls(negative);
const comparison = fixtures.find(({ name }) => name === "comparison");
assert.ok(comparison);
assertSwitchBoundary({ ...boundary, jump_tables: [] }, comparison);
assert.throws(
() => assertSwitchBoundary(boundary, comparison),
undefined,
"Fabricated table for comparison-only control",
);
return [
...assertUnsafeGuardControls(fixtures),
...unsignedControls,
...Object.keys(faults),
"signed_label_magnitude",
"fabricated_comparison_table",
];
}
function assertStrippedSignednessControls(fixture) {
const strippedFixture = { ...fixture, allow_unsigned_32_labels: true };
const signed = guardBoundary(fixture);
const unsigned = structuredClone(signed);
unsigned.parameters = [
{ data_type: "undefined4", size_bytes: 4, confidence: "low" },
];
for (const mapping of unsigned.jump_tables[0].mappings) {
const previous = mapping.case_value;
mapping.case_value = Number(BigInt.asUintN(32, BigInt(previous)));
mapping.evidence[0].detail = mapping.evidence[0].detail.replace(
`Typed case value ${previous} `,
`Typed case value ${mapping.case_value} `,
);
}
assertSwitchBoundary(unsigned, strippedFixture);
assert.throws(
() => assertSwitchBoundary(unsigned, fixture),
undefined,
"Known signed source labels were replaced by unsigned values",
);
const faults = {
stripped_signedness_promoted: (record) => {
record.parameters[0].confidence = "high";
},
stripped_wrong_discriminator_width: (record) => {
record.parameters[0].size_bytes = 8;
},
stripped_wrong_bit_pattern_target: (record) => {
record.jump_tables[0].mappings[0].target_address =
fixture.blocks[1].address;
},
stripped_duplicate_bit_pattern: (record) => {
record.jump_tables[0].mappings.push(signed.jump_tables[0].mappings[0]);
},
};
for (const [name, inject] of Object.entries(faults)) {
const damaged = structuredClone(unsigned);
inject(damaged);
assert.throws(
() => assertSwitchBoundary(damaged, strippedFixture),
undefined,
`Oracle failed to reject ${name}`,
);
}
return ["known_signed_labels_must_stay_signed", ...Object.keys(faults)];
}
function assertUnsafeGuardControls(fixtures) {
const unsafe = fixtures.find(({ name }) => name === "unsafe_integer");
assert.ok(unsafe);
const unknown = guardBoundary(unsafe, true);
assertSwitchBoundary(unknown, unsafe);
for (const labels of [
unsafe.slots.map(({ value }) => Number(BigInt(value) & 0xffffffffn)),
unsafe.slots.map((_, index) => index),
unsafe.slots.map(({ value }) => Number(value)),
]) {
const fabricated = structuredClone(unknown);
fabricated.jump_tables[0].mappings = fabricated.jump_tables[0].mappings.map(
(mapping, index) => ({
...mapping,
case_value: labels[index],
confidence: "high",
}),
);
assert.throws(
() => assertSwitchBoundary(fabricated, unsafe),
undefined,
"Unsafe label rounding/truncation/index fallback accepted",
);
}
return [
"unsafe_label_signed32_truncation",
"unsafe_label_index_fallback",
"unsafe_label_float_rounding",
];
}
function guardBoundary(fixture, unresolved = false) {
const typedEvidence = (label, target) => [
{
kind: "jump-table",
source: "ghidra-clang-case-token",
detail: `${label} belongs to the p-code block starting at recovered target ${target}; its unique indirect dispatch predecessor is ${fixture.dispatch_address}.`,
},
];
const unknownEvidence = (target) => [
{
kind: "jump-table",
source: "ghidra-high-function",
detail: `Ghidra recovered target ${target} at dispatch ${fixture.dispatch_address}, but no unambiguous, exactly representable case label or default relationship was available.`,
},
];
return {
available: true,
provenance: "ghidra-high-function",
limitations: ["Guard control only"],
jump_tables: [
{
dispatch_address: fixture.dispatch_address,
data_sources: [
{
address: fixture.table_address,
entry_size_bytes: 8,
entry_count: fixture.slots.length,
},
],
mappings: fixture.slots.map(({ value, target_address }, index) => ({
case_value: unresolved ? null : value,
target_address,
confidence: unresolved
? index % 2 === 0
? "medium"
: "low"
: "high",
evidence: unresolved
? unknownEvidence(target_address)
: typedEvidence(`Typed case value ${value}`, target_address),
})),
default_targets: [
{
target_address: fixture.default_address,
confidence: "high",
evidence: typedEvidence(
"Typed default label",
fixture.default_address,
),
},
],
},
],
};
}
@@ -0,0 +1,154 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { mkdir, readFile, realpath } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const EXPECTED_CHECKS = [
"positive_decimal",
"unsigned32_literal_not_rewritten",
"negative_embedded_token_sign",
"negative_hex",
"zero",
"magnitude_mismatch_unknown",
"signed_encoding_is_not_magnitude",
"symbol_label_unknown",
"compound_label_unknown",
"safe_positive_boundary",
"safe_negative_boundary",
"safe_hex_boundary",
"unsafe_positive_unknown",
"unsafe_negative_unknown",
"unsigned64_unknown",
"no_dispatch_not_attributed",
"unique_dispatch_block_start",
"instruction_offset_is_not_destination",
"wrong_dispatch_not_attributed",
"target_not_in_table_not_attributed",
"signed_and_positive_distinct",
"shared_destination_keeps_all_labels",
"default_ordinary_off_is_role",
"default_and_numeric_can_share_target",
"unsafe64_target_preserved_unknown",
"magnitude_mismatch_target_preserved_unknown",
"conflicting_value_retracted",
"conflicting_destinations_preserved_unknown",
"api_first_dispatch_is_insufficient",
"multiple_dispatches_rejected",
"multiple_dispatch_target_preserved_unknown",
"multiple_dispatch_rejected_for_other_table",
];
/** Exercise actual bridge methods on detached Ghidra objects with owned process cleanup. */
export async function verifySwitchEvidenceControls({
installation,
entrypoint,
target,
workspace,
runId,
}) {
const { spawnOwnedProviderProcess, ProviderProcessSupervisor } =
await import("../dist/process/ProviderProcess.js");
const { cleanupOwnedProcessGroup } =
await import("../dist/process/ProcessOwnership.js");
const bridge = await realpath(
join(dirname(entrypoint), "../bridge/ghidra/ReaGhidraBridge.java"),
);
const fixtures = fileURLToPath(
new URL("../tests/conformance/ghidra", import.meta.url),
);
const project = join(workspace, "guard-project");
await mkdir(project);
const sourceDigest = digest(await readFile(bridge));
const targetDigest = digest(await readFile(target));
const launch = await spawnOwnedProviderProcess({
command: installation.analyzeHeadlessPath,
arguments: [
project,
"SwitchEvidence",
"-import",
target,
"-noanalysis",
"-readOnly",
"-scriptPath",
`${fixtures};${dirname(bridge)}`,
"-postScript",
"ReaSwitchEvidenceProbe.java",
bridge,
"-deleteProject",
],
runId,
expectedCommand: null,
});
const supervisor = new ProviderProcessSupervisor({
...launch,
ownsProcessLifetime: true,
cleanup: () => cleanupOwnedProcessGroup(launch.ownership),
});
try {
assert.ok(
await supervisor.waitForExit(120000),
"Production switch guard lane exceeded its 120-second headless deadline",
);
const snapshot = supervisor.snapshot();
const output = `${snapshot.stdout.text}\n${snapshot.stderr.text}`;
assert.equal(snapshot.exitCode, 0, output);
const reports = [
...output.matchAll(/REA_SWITCH_EVIDENCE_PROBE_JSON (\{[^\r\n]*\})/gu),
];
assert.equal(
reports.length,
1,
`Expected one production guard report: ${output}`,
);
assert.ok(output.includes("REA_SWITCH_EVIDENCE_PROBE_COMPLETE"), output);
const report = JSON.parse(reports[0][1]);
assertControlReport(
report,
bridge,
sourceDigest,
installation.providerVersion,
);
assert.equal(
digest(await readFile(bridge)),
sourceDigest,
"Guard modified bridge source",
);
assert.equal(
digest(await readFile(target)),
targetDigest,
"Guard modified imported fixture bytes",
);
return report;
} finally {
const stopped = await supervisor.stop();
supervisor.dispose();
assert.notEqual(stopped.status, "incomplete", JSON.stringify(stopped));
}
}
function digest(bytes) {
return createHash("sha256").update(bytes).digest("hex");
}
function assertControlReport(report, bridge, sourceDigest, version) {
assert.ok(
typeof report === "object" && report !== null && !Array.isArray(report),
);
assert.equal(report.schema_version, 1);
assert.equal(report.status, "passed");
assert.equal(report.ghidra_version, version);
assert.equal(report.bridge_source_path, bridge);
assert.equal(report.bridge_source_sha256, sourceDigest);
assert.ok(Array.isArray(report.checks));
assert.deepEqual(
[...report.checks].sort(),
[...EXPECTED_CHECKS].sort(),
"Production guard did not execute every required case",
);
assert.equal(report.check_count, EXPECTED_CHECKS.length);
assert.ok(
typeof report.scope === "string" &&
report.scope.includes("detached Ghidra model objects"),
);
}
+347
View File
@@ -0,0 +1,347 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import {
copyFile,
mkdir,
mkdtemp,
readFile,
readdir,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { parseArgs, promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import {
buildSwitchSource,
switchSymbolOracle,
assertCompiledSwitchOracle,
} from "../tests/conformance/ghidra/switch-fixture.mjs";
import {
assertSwitchBoundary,
assertSwitchOracleControls,
} from "./verify-real-ghidra-switch-assertions.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
import { verifySwitchEvidenceControls } from "./verify-real-ghidra-switch-controls.mjs";
// Narrow compiler-shape lane; no cross compiler or game artifact prerequisites.
const { values } = parseArgs({
options: {
"compiler-oracle-only": { type: "boolean", default: false },
entrypoint: { type: "string" },
},
});
const compilerOnly = values["compiler-oracle-only"];
const entrypoint =
values.entrypoint === undefined
? fileURLToPath(new URL("./rea.mjs", import.meta.url))
: resolve(values.entrypoint);
assert.ok(
process.platform === "linux" && process.arch === "x64",
"Ghidra switch compiler lane requires Linux x64; macOS/Windows and cross targets use their separate provider lanes.",
);
const exec = promisify(execFile);
const commands = {
gcc: process.env.REA_GCC ?? "gcc",
clang: process.env.REA_CLANG ?? "clang",
nm: process.env.REA_NM ?? "nm",
objdump: process.env.REA_OBJDUMP ?? "objdump",
strip: process.env.REA_STRIP ?? "strip",
};
const versions = {};
for (const [role, command] of Object.entries(commands)) {
try {
const { stdout } = await exec(command, ["--version"]);
versions[role] = stdout.split("\n")[0];
} catch (cause) {
throw new Error(
`Ghidra switch compiler lane prerequisite missing: ${role} '${command}'`,
{ cause },
);
}
}
let installation;
if (!compilerOnly) {
const { inspectGhidraInstallation } =
await import("../dist/ghidra/GhidraInstallation.js");
assert.ok(
process.env.GHIDRA_INSTALL_DIR,
"Ghidra switch lane prerequisite missing: GHIDRA_INSTALL_DIR (bring your own)",
);
installation = inspectGhidraInstallation({
installDir: process.env.GHIDRA_INSTALL_DIR,
...(process.env.JAVA_HOME === undefined
? {}
: { javaHome: process.env.JAVA_HOME }),
});
assert.equal(installation.status, "available", JSON.stringify(installation));
}
const run = createVerifierRun();
const workspace = await mkdtemp(join(tmpdir(), "rea-switch-proof-"));
const runtime = join(workspace, "runtime");
await mkdir(runtime);
const source = join(workspace, "switch.c");
await writeFile(source, buildSwitchSource());
const flags = ["-O2", "-g", "-fno-pie", "-no-pie"];
const results = [];
let productionControls = null;
try {
for (const compiler of ["gcc", "clang"]) {
const original = join(workspace, `${compiler}-optimized`);
await exec(commands[compiler], [...flags, source, "-o", original]);
const bytes = await readFile(original);
const { stdout: symbols } = await exec(commands.nm, ["-n", original]);
const { stdout: disassembly } = await exec(commands.objdump, [
"-d",
original,
]);
const fixtures = assertCompiledSwitchOracle(
bytes,
switchSymbolOracle(symbols),
disassembly,
);
const oracleFaultControls = assertSwitchOracleControls(fixtures);
if (!compilerOnly && productionControls === null) {
productionControls = await verifySwitchEvidenceControls({
installation,
entrypoint,
target: original,
workspace,
runId: run.run_id,
});
assert.equal(
digest(await readFile(original)),
digest(bytes),
"Guard changed compiler-oracled input",
);
}
const stripped = join(workspace, `${compiler}-stripped`);
await copyFile(original, stripped);
await exec(commands.strip, ["--strip-all", stripped]);
// Byte/physical target oracle survives removal of the names that REA could use.
assertCompiledSwitchOracle(await readFile(stripped), fixtures, disassembly);
for (const [variant, path] of [
["optimized", original],
["stripped", stripped],
]) {
const sha256 = digest(await readFile(path));
const observation = compilerOnly
? null
: await verifyTarget(path, sha256, fixtures, variant);
assert.equal(
digest(await readFile(path)),
sha256,
"REA analysis changed original compiled fixture",
);
results.push({
compiler,
variant,
oracle_fault_controls: oracleFaultControls,
sha256,
compiler_oracle: fixtures.map(
({
name,
address,
dispatch_address,
table_address,
slots,
default_address,
}) => ({
name,
address,
dispatch_address,
table_address,
slots,
default_address,
}),
),
observation,
});
}
}
} finally {
try {
assert.deepEqual(
(await readdir(runtime)).filter((name) => name.startsWith("rea-ghidra-")),
[],
"Owned Ghidra runtime survived close",
);
} finally {
await rm(workspace, { recursive: true, force: true });
}
}
const lifecycle = await completeVerifierRun(run);
if (lifecycle.process_lineage.status === "verified")
assert.deepEqual(lifecycle.process_lineage.descendants, []);
process.stdout.write(
`${JSON.stringify({ lane: "linux-x64-compiler-switch", compiler_only: compilerOnly, entrypoint, versions, compiler_flags: flags, verifier_run: lifecycle, production_controls: productionControls, results, limitations: ["Exact compiler oracle admits absolute 8-byte native x86-64 ELF tables and fails explicitly when a compiler changes lowering.", "Compiled binaries are analyzed read-only; target programs are never executed.", "Comparison-only control proves absence of a fabricated table and preservation of provider limitations, not recovery of C source.", "Detached Ghidra object controls exercise actual bridge methods through reflection; they do not claim naturally compiled ambiguous dispatches.", "CLI/MCP comparison covers switch observations, excluding process-specific Evidence IDs."], cleanup: "complete" })}\n`,
);
function digest(bytes) {
return createHash("sha256").update(bytes).digest("hex");
}
async function verifyTarget(path, sha256, fixtures, variant) {
const env = {
PATH: process.env.PATH ?? "/usr/bin:/bin",
TMPDIR: runtime,
REA_LOG_LEVEL: "silent",
REA_ANALYSIS_PROVIDER: "ghidra",
REA_PROCESS_RUN_ID: run.run_id,
GHIDRA_INSTALL_DIR: process.env.GHIDRA_INSTALL_DIR,
...(process.env.JAVA_HOME === undefined
? {}
: { JAVA_HOME: process.env.JAVA_HOME }),
};
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
const client = new Client({ name: "rea-real-switch-proof", version: "1" });
let opened = false;
let stderr = "";
const observations = [];
try {
await client.connect(transport);
transport.stderr?.on("data", (chunk) => {
stderr = (stderr + chunk.toString()).slice(-65536);
});
const target = await call(client, "open_binary", {
path,
provider: "ghidra",
});
opened = true;
assert.equal(target.sha256, sha256);
assert.equal(target.architecture, "x86_64");
assert.equal(target.format, "elf");
const procedures = await call(client, "list_procedures");
for (const fixture of fixtures) {
const observationFixture = {
...fixture,
allow_unsigned_32_labels:
variant === "stripped" && fixture.name === "negative",
};
const procedure = procedures.find(
({ address }) => address === fixture.address,
);
assert.ok(
procedure,
`Source-owned function at ${fixture.address} not discovered in optimized/stripped fixture`,
);
const observed = await toolResult(client, "inspect_native_api", {
procedure: procedure.address,
});
assertWorkflowIdentity(observed.evidence, sha256);
assert.equal(observed.result.procedure.address, procedure.address);
const summary = assertSwitchBoundary(
observed.result.boundary,
observationFixture,
);
assert.deepEqual(
[...observed.result.residual_unknowns].sort(),
expectedResidualUnknowns(observed.result.boundary, fixture).sort(),
"Residual unknowns must preserve unresolved ABI types and exactly the expected switch uncertainty",
);
const cli = await cliSwitchEvidence(
entrypoint,
path,
procedure.address,
env,
);
assertWorkflowIdentity(cli, sha256);
assert.equal(cli.normalized_result.procedure.address, procedure.address);
assertSwitchBoundary(cli.normalized_result.boundary, observationFixture);
assert.deepEqual(
cli.normalized_result.boundary.jump_tables,
observed.result.boundary.jump_tables,
"CLI/MCP switch mappings/defaults/data-source evidence differ",
);
assert.deepEqual(
cli.normalized_result.residual_unknowns,
observed.result.residual_unknowns,
);
observations.push({
name: fixture.name,
...summary,
cli_mcp_equal: true,
limitations: observed.result.boundary.limitations,
residual_unknowns: observed.result.residual_unknowns,
jump_tables: observed.result.boundary.jump_tables,
});
}
return { observations, stderr };
} finally {
try {
if (opened) await call(client, "close_binary");
} finally {
try {
await client.close();
} finally {
await transport.close();
}
}
}
}
function assertWorkflowIdentity(evidence, sha256) {
assert.equal(evidence.subject.digest.sha256, sha256);
assert.equal(evidence.operation, "inspect_native_api");
assert.equal(evidence.provider.id, "rea-workflow");
assert.equal(evidence.analysis_profile.provider.id, "rea-workflow");
assert.equal(
evidence.analysis_profile.parameters.upstream_analysis_profile.parameters
.language_id,
"auto-from-header",
);
}
function expectedResidualUnknowns(boundary, fixture) {
const unknowns = [boundary.return_type, ...boundary.parameters]
.filter(({ confidence }) => confidence === "low")
.map(
({ role, data_type: type }) =>
`Can the inferred ${role} type ${type} be confirmed with imported metadata or an ABI probe?`,
);
if (fixture.unsafe_integer)
unknowns.push(
`Which source-level case values correspond to every target dispatched at ${boundary.jump_tables[0].dispatch_address}?`,
);
return [...new Set(unknowns)];
}
async function toolResult(client, name, arguments_ = {}) {
const result = await client.callTool(
{ name, arguments: arguments_ },
{ timeout: 240000 },
);
assert.notEqual(result.isError, true, JSON.stringify(result));
assert.ok(
result.structuredContent && "result" in result.structuredContent,
`${name} omitted structured result`,
);
return result.structuredContent;
}
async function call(client, name, arguments_ = {}) {
return (await toolResult(client, name, arguments_)).result;
}
async function cliSwitchEvidence(entrypoint, path, procedure, env) {
const { stdout } = await exec(
process.execPath,
[
entrypoint,
"inspect-native-api",
path,
procedure,
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
);
return JSON.parse(stdout);
}
+49 -14
View File
@@ -33,8 +33,10 @@ import {
verifyInventoryOperations,
} from "./verify-real-ghidra-inventory.mjs";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
import { hasTypedSwitchEvidence } from "./verify-real-ghidra-switch-assertions.mjs";
import {
verifyDenseDefaultReturn,
verifyNativeTypeLayout,
verifyRelativeSwitch,
verifyNativeValueTrace,
@@ -308,8 +310,7 @@ function nativeFixtureTarget(platform, architecture) {
async function verifyNativeApiCli(
targetPath,
procedures,
requireDenseJumpTable,
denseSwitchSymbol = "rea_ghidra_inventory_dense_switch",
{ requireDenseJumpTable, denseSwitchSymbol, client },
) {
const denseSwitch = procedures.find(({ value }) =>
value.endsWith(denseSwitchSymbol),
@@ -349,11 +350,25 @@ async function verifyNativeApiCli(
(mapping) =>
mapping.case_value === caseValue &&
mapping.confidence === "high" &&
mapping.evidence.some(
({ detail }) =>
detail.includes(`case value ${caseValue} with target`) ||
detail.startsWith(`Case value ${caseValue} indexes byte `),
),
(hasTypedSwitchEvidence(
mapping.evidence,
`Typed case value ${caseValue}`,
mapping.target_address,
denseTable.dispatch_address,
) ||
mapping.evidence.some(
({ kind, source, detail }) =>
kind === "jump-table" &&
typeof detail === "string" &&
detail.trim().length > 0 &&
((source === "ghidra-high-function" &&
detail.includes(
`case value ${caseValue} with target ${mapping.target_address}`,
)) ||
(source === "ghidra-instruction-and-memory" &&
detail.startsWith(`Case value ${caseValue} indexes byte `) &&
detail.includes(`reaching ${mapping.target_address}.`))),
)),
),
);
if (
@@ -368,7 +383,20 @@ async function verifyNativeApiCli(
if (
requireDenseJumpTable &&
(denseTable === undefined ||
denseCases.size !== denseTable.mappings.length ||
denseCases.size !== expectedCases.length ||
denseTable.mappings.length !== expectedCases.length ||
denseTable.default_targets.length !== 1 ||
denseTable.default_targets[0].confidence !== "high" ||
!hasTypedSwitchEvidence(
denseTable.default_targets[0].evidence,
"Typed default label",
denseTable.default_targets[0].target_address,
denseTable.dispatch_address,
) ||
denseTable.mappings.some(
({ target_address }) =>
target_address === denseTable.default_targets[0].target_address,
) ||
expectedCases.some((caseValue) => !denseCases.has(caseValue)) ||
!mappingsAreExact ||
evidence.normalized_result?.residual_unknowns?.length !== 0)
@@ -376,10 +404,17 @@ async function verifyNativeApiCli(
throw new Error(
`The shipped inspect-native-api CLI did not preserve complete dense jump-table output: ${stdout}`,
);
const defaultOracle = requireDenseJumpTable
? await verifyDenseDefaultReturn(
client,
denseTable.default_targets[0].target_address,
)
: null;
return {
operation: evidence.operation,
provider: evidence.provider,
mappings_returned: denseTable?.mappings.length ?? null,
default_oracle: defaultOracle,
residual_unknowns: evidence.normalized_result.residual_unknowns.length,
};
}
@@ -456,16 +491,16 @@ async function verifyTarget(targetPath, variant, expectedTarget = null) {
variant === "cross-arm64-elf" || variant === "aarch64-jump-table";
const nativeApiCli =
variant === "debug" || crossArm64Elf
? await verifyNativeApiCli(
parsedTarget.value.path,
procedures,
crossArm64Elf ||
? await verifyNativeApiCli(parsedTarget.value.path, procedures, {
requireDenseJumpTable:
crossArm64Elf ||
(expectedNativeTarget.format === "elf" &&
expectedNativeTarget.architecture === "x86_64"),
crossArm64Elf
denseSwitchSymbol: crossArm64Elf
? "rea_cross_dense_switch"
: "rea_ghidra_inventory_dense_switch",
)
client,
})
: null;
const probes = variant.startsWith("relative-")
? await verifyRelativeSwitch(
-356
View File
@@ -1,356 +0,0 @@
#!/usr/bin/env node
import { readFile, realpath } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { PermissionAuthority } from "../dist/application/PermissionAuthority.js";
import { runControlledReplay } from "../dist/application/JavaScriptReplayService.js";
import { createPermissionPolicy } from "../dist/domain/permissionPolicy.js";
import { LinuxJavaScriptReplayRunner } from "../dist/replay/LinuxJavaScriptReplayRunner.js";
import { SystemJavaScriptReplayHost } from "../dist/replay/SystemJavaScriptReplayHost.js";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
const verifierRun = createVerifierRun();
const suppliedInputPath = process.env.REA_REPLAY_INPUT_PATH;
const suppliedInput =
suppliedInputPath === undefined
? undefined
: JSON.parse(await readFile(await realpath(suppliedInputPath), "utf8"));
const canonicalSide = async (side) => ({
...side,
modules: await Promise.all(
side.modules.map(async (module) => ({
...module,
path: await realpath(module.path),
})),
),
});
const leftPath = await realpath(
process.env.REA_REPLAY_LEFT_PATH ??
resolve("tests/fixtures/replay/parser.mjs"),
);
const rightPath = await realpath(
process.env.REA_REPLAY_RIGHT_PATH ??
resolve("tests/fixtures/replay/parser-v2.mjs"),
);
const suppliedLeft =
suppliedInput === undefined
? undefined
: await canonicalSide(suppliedInput.left);
const suppliedRight =
suppliedInput?.right === undefined
? undefined
: await canonicalSide(suppliedInput.right);
const roots = [
...new Set(
suppliedLeft === undefined
? [dirname(leftPath), dirname(rightPath)]
: [...suppliedLeft.modules, ...(suppliedRight?.modules ?? [])].map(
({ path }) => dirname(path),
),
),
];
const policy = {
status: "enabled",
roots,
nodePath: process.execPath,
bubblewrapPath:
process.env.REA_JAVASCRIPT_REPLAY_BWRAP_PATH ?? "/usr/bin/bwrap",
systemdRunPath:
process.env.REA_JAVASCRIPT_REPLAY_SYSTEMD_RUN_PATH ??
"/usr/bin/systemd-run",
systemctlPath:
process.env.REA_JAVASCRIPT_REPLAY_SYSTEMCTL_PATH ?? "/usr/bin/systemctl",
shellPath: process.env.REA_JAVASCRIPT_REPLAY_SHELL_PATH ?? "/usr/bin/bash",
};
const ceiling = {
capability: "javascript_replay",
roots,
executables: [
policy.nodePath,
policy.bubblewrapPath,
policy.systemdRunPath,
policy.systemctlPath,
policy.shellPath,
],
environment_names: [],
network: "none",
mount: true,
};
const authority = new PermissionAuthority(
createPermissionPolicy(
[ceiling],
[
{
...ceiling,
grant_id: "administrator:javascript_replay",
lifetime: "administrator",
operation_identity: null,
expires_at: null,
},
],
),
);
const dependencies = {
policy: () => policy,
host: new SystemJavaScriptReplayHost(),
runner: new LinuxJavaScriptReplayRunner(),
authority,
};
const defaultInput = {
mode: "plan",
left: {
modules: [
{
alias: "parser",
path: leftPath,
format: "esm",
role: "module",
dependencies: {},
},
],
entry_alias: "parser",
entry_export: "default",
},
right: {
modules: [
{
alias: "parser",
path: rightPath,
format: "esm",
role: "module",
dependencies: {},
},
],
entry_alias: "parser",
entry_export: "default",
},
cases: [
{ case_id: "heading", arguments: ["# Title"] },
{ case_id: "plain", arguments: ["plain"] },
],
generator: { preset: "parser-boundaries", seed: 7, count: 4 },
};
const suppliedPlanInput =
suppliedInput === undefined
? undefined
: (({ approved: _approved, plan_digest: _planDigest, ...planInput }) =>
planInput)(suppliedInput);
const input =
suppliedPlanInput === undefined
? defaultInput
: {
...suppliedPlanInput,
mode: "plan",
left: suppliedLeft,
...(suppliedRight === undefined ? {} : { right: suppliedRight }),
reproducer_export: undefined,
};
const planned = await runControlledReplay(dependencies, input);
if (!planned.ok) {
if (process.env.REA_REPLAY_ALLOW_UNAVAILABLE === "true") {
process.stdout.write(
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), available: false, error_tag: planned.error._tag })}\n`,
);
process.exit(0);
}
throw planned.error;
}
const plan = planned.value.plan;
if (plan === null || typeof plan.plan_digest !== "string")
throw new Error("Real replay verifier did not receive a plan");
const executed = await runControlledReplay(dependencies, {
...input,
mode: "execute",
plan_digest: plan.plan_digest,
});
if (!executed.ok) throw executed.error;
const evidence = executed.value.evidence;
if (
evidence === null ||
evidence.authority !== "controlled-replay" ||
evidence.environment?.isolation !== "container" ||
evidence.normalized_result.termination !== "completed" ||
evidence.normalized_result.cleanup.state !== "complete" ||
evidence.normalized_result.comparison?.some(
({ status }) => status === "unknown",
) === true
)
throw new Error(
`Real replay verifier received incomplete Evidence: ${JSON.stringify({
authority: evidence?.authority ?? null,
isolation: evidence?.environment?.isolation ?? null,
termination: evidence?.normalized_result?.termination ?? null,
cleanup: evidence?.normalized_result?.cleanup?.state ?? null,
outcomes:
evidence?.normalized_result?.outcomes?.map(({ outcome }) => outcome) ??
[],
stderr: evidence?.normalized_result?.stderr ?? null,
comparison_statuses:
evidence?.normalized_result?.comparison?.map(({ status }) => status) ??
[],
})}`,
);
if (suppliedInput !== undefined) {
process.stdout.write(
`${JSON.stringify({
verifier_run: await completeVerifierRun(verifierRun),
available: true,
supplied_manifest: true,
plan_digest: plan.plan_digest,
evidence_id: evidence.evidence_id,
source_evidence_ids: executed.value.source_evidence.map(
({ evidence_id: id }) => id,
),
left_sha256: plan.left.modules.map(({ sha256 }) => sha256),
right_sha256: plan.right?.modules.map(({ sha256 }) => sha256) ?? [],
cases: plan.cases.length,
comparison:
evidence.normalized_result.comparison?.map(({ case_id, status }) => ({
case_id,
status,
})) ?? [],
cleanup: evidence.normalized_result.cleanup.state,
})}\n`,
);
process.exit(0);
}
const runSingle = async (path, limits = undefined) => {
const manifest = {
mode: "plan",
left: {
modules: [
{
alias: "entry",
path: await realpath(path),
format: "esm",
role: "module",
dependencies: {},
},
],
entry_alias: "entry",
entry_export: "default",
},
cases: [{ case_id: "probe", arguments: [] }],
...(limits === undefined ? {} : { limits }),
};
const proposed = await runControlledReplay(dependencies, manifest);
if (!proposed.ok || proposed.value.plan === null)
throw new Error("Replay conformance plan failed");
const observed = await runControlledReplay(dependencies, {
...manifest,
mode: "execute",
plan_digest: proposed.value.plan.plan_digest,
});
if (!observed.ok || observed.value.evidence === null)
throw new Error("Replay conformance execution failed");
return observed.value.evidence.normalized_result;
};
const sideEffects = await runSingle(
resolve("tests/fixtures/replay/side-effect-attempt.mjs"),
);
if (
sideEffects.outcomes[0]?.outcome !== "return" ||
Object.values(sideEffects.outcomes[0].value).some(
(value) => value !== "undefined",
)
)
throw new Error("Ambient side-effect surface was exposed to replay code");
const timeout = await runSingle(
resolve("tests/fixtures/replay/busy-loop.mjs"),
{
wall_time_ms: 100,
},
);
if (timeout.termination !== "timeout" || timeout.cleanup.state !== "complete")
throw new Error("Replay timeout did not kill and clean the complete sandbox");
const differentialTimeoutInput = {
mode: "plan",
left: {
modules: [
{
alias: "entry",
path: await realpath(resolve("tests/fixtures/replay/busy-loop.mjs")),
format: "esm",
role: "module",
dependencies: {},
},
],
entry_alias: "entry",
entry_export: "default",
},
right: {
modules: [
{
alias: "entry",
path: await realpath(resolve("tests/fixtures/replay/parser.mjs")),
format: "esm",
role: "module",
dependencies: {},
},
],
entry_alias: "entry",
entry_export: "default",
},
cases: [{ case_id: "probe", arguments: [] }],
limits: { wall_time_ms: 100 },
};
const differentialTimeoutPlan = await runControlledReplay(
dependencies,
differentialTimeoutInput,
);
if (!differentialTimeoutPlan.ok || differentialTimeoutPlan.value.plan === null)
throw new Error("Differential timeout plan failed");
const differentialTimeout = await runControlledReplay(dependencies, {
...differentialTimeoutInput,
mode: "execute",
plan_digest: differentialTimeoutPlan.value.plan.plan_digest,
});
if (
!differentialTimeout.ok ||
differentialTimeout.value.evidence === null ||
differentialTimeout.value.source_evidence.length !== 2 ||
differentialTimeout.value.evidence.normalized_result.comparison?.[0]
?.status !== "unknown" ||
differentialTimeout.value.source_evidence.some(
(item) => item.normalized_result.outcomes[0]?.outcome !== "timeout",
)
)
throw new Error("Differential termination did not retain both observations");
const memory = await runSingle(
resolve("tests/fixtures/replay/memory-pressure.mjs"),
{ wall_time_ms: 3000, memory_bytes: 32 * 1024 * 1024 },
);
if (memory.termination !== "oom" || memory.cleanup.state !== "complete")
throw new Error(
`Replay OOM did not report and clean the cgroup limit: ${JSON.stringify(memory)}`,
);
process.stdout.write(
`${JSON.stringify({
verifier_run: await completeVerifierRun(verifierRun),
available: true,
plan_digest: plan.plan_digest,
evidence_id: evidence.evidence_id,
left_sha256: plan.left.modules[0]?.sha256,
right_sha256: plan.right?.modules[0]?.sha256,
cases: plan.cases.length,
comparison: evidence.normalized_result.comparison?.map(
({ case_id, status }) => ({ case_id, status }),
),
cleanup: evidence.normalized_result.cleanup.state,
side_effect_surface: "unavailable",
timeout: timeout.termination,
differential_timeout:
differentialTimeout.value.evidence.normalized_result.comparison[0]
?.status,
memory: memory.termination,
})}\n`,
);
@@ -1,82 +1,179 @@
import assert from "node:assert/strict";
import { execFile, spawn } from "node:child_process";
import { once } from "node:events";
import { dirname, resolve } from "node:path";
import { mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
const execute = promisify(execFile);
const root = resolve(
dirname(fileURLToPath(import.meta.url)),
"../tests/conformance/runtime-inspector",
);
const targetPath = resolve(root, "node-target.mjs");
const child = spawn(process.execPath, ["--inspect=127.0.0.1:0", targetPath], {
stdio: ["ignore", "ignore", "pipe"],
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const root = await realpath(await mkdtemp(join(tmpdir(), "rea-inspector-")));
const environment = {
...process.env,
};
const client = new Client({
name: "rea-inspector-conformance",
version: "1.0.0",
});
const transport = new StdioClientTransport({
command: process.execPath,
args: ["scripts/rea.mjs", "mcp"],
cwd: repository,
env: environment,
stderr: "pipe",
});
const names = [
"node-target.mjs",
"plain.cjs",
"with space.cjs",
"with_under_score.cjs",
"with#hash.cjs",
"with%percent.cjs",
"with%23hash.cjs",
...(process.platform === "win32" ? [] : ['with"quote.cjs']),
];
try {
const webSocketUrl = await inspectorUrl(child);
const socket = new URL(webSocketUrl);
await client.connect(transport);
for (const name of names) {
const targetPath =
name === "node-target.mjs"
? await realpath(
join(repository, "tests/conformance/runtime-inspector", name),
)
: join(root, name);
if (name !== "node-target.mjs")
await writeFile(targetPath, "setInterval(() => {}, 1000);\n");
const child = spawn(
process.execPath,
["--inspect=127.0.0.1:0", targetPath],
{
stdio: ["ignore", "ignore", "pipe"],
},
);
try {
await verifyTarget(child, targetPath, name);
} finally {
await terminate(child);
}
}
} finally {
try {
await client.close();
} finally {
await rm(root, { recursive: true, force: true });
}
}
async function verifyTarget(child, targetPath, name) {
const socket = new URL(await inspectorUrl(child));
const endpoint = `http://127.0.0.1:${socket.port}`;
const environment = {
...process.env,
REA_V8_INSPECTOR_OBSERVE_ENABLED: "true",
};
const listed = await runCli(
["list-javascript-runtime-targets", endpoint, "--json"],
environment,
);
const target = listed.normalized_result?.targets?.[0];
if (
listed.operation !== "list_javascript_runtime_targets" ||
typeof target?.target_id !== "string" ||
target.location?.kind !== "file" ||
target.location.file_path !== targetPath
)
throw new Error("Real Node Inspector target discovery did not match");
const observed = await runCli(
[
"observe-javascript-runtime",
endpoint,
target.target_id,
"--runtime-kind",
"node",
"--observation-ms",
"100",
"--json",
],
environment,
);
const result = observed.normalized_result;
if (
observed.operation !== "observe_javascript_runtime" ||
observed.provider?.id !== "rea-v8-inspector" ||
result?.target?.target_id !== target.target_id ||
result?.target?.runtime_kind !== "node" ||
!Array.isArray(result?.scripts?.items) ||
result.scripts.items.length === 0
)
throw new Error("Real Node Inspector observation did not match");
if (
!Array.isArray(result.directly_observed) ||
!Array.isArray(result.unavailable_without_instrumentation) ||
result.capture?.truncated !== false
)
throw new Error("Real Node Inspector capture was incomplete");
const response = await fetch(`${endpoint}/json/list`);
assert.equal(response.ok, true);
const discovery = await response.json();
assert.equal(discovery.length, 1);
const raw = discovery[0];
assert.equal(typeof raw.id, "string");
// Independent oracle: the fixture owns the pathname, and Node's discovery
// serialization loses quotes/backslashes. Never decode underscores into it.
const unresolved = process.platform === "win32" || /[_"\\]/u.test(targetPath);
const expectedLocation = unresolved
? {
kind: "unresolved",
reported_url: raw.url,
reason: "unverifiable-file-location",
}
: { kind: "file", file_path: targetPath };
const listed = await runCli([
"list-javascript-runtime-targets",
endpoint,
"--json",
]);
assert.equal(listed.operation, "list_javascript_runtime_targets");
const inventory = listed.normalized_result;
assert.equal(inventory.targets.length, 1);
assert.equal(inventory.targets[0].target_id, raw.id);
assert.deepEqual(inventory.targets[0].location, expectedLocation);
assert.equal(inventory.excluded.unsupported_location, 0);
const mcpList = await runMcp("list_javascript_runtime_targets", {
inspector_endpoint: endpoint,
});
assert.deepEqual(mcpList, inventory);
const observed = await runCli([
"observe-javascript-runtime",
endpoint,
raw.id,
"--runtime-kind",
"node",
"--observation-ms",
"100",
"--json",
]);
assert.equal(observed.operation, "observe_javascript_runtime");
assert.equal(observed.provider.id, "rea-v8-inspector");
const mcpObservation = await runMcp("observe_javascript_runtime", {
inspector_endpoint: endpoint,
target_id: raw.id,
runtime_kind: "node",
observation_ms: 100,
});
for (const result of [observed.normalized_result, mcpObservation])
verifyObservation(result, raw.id, expectedLocation, targetPath);
assert.deepEqual(mcpObservation.target, observed.normalized_result.target);
process.stdout.write(
`${JSON.stringify({
status: "pass",
provider: observed.provider.id,
target_id: target.target_id,
platform: process.platform,
node: process.version,
fixture: name,
target_id: raw.id,
discovery_url: raw.url,
target_location: expectedLocation,
cli_scripts: observed.normalized_result.scripts.items.length,
mcp_scripts: mcpObservation.scripts.items.length,
listed_evidence_id: listed.evidence_id,
observation_evidence_id: observed.evidence_id,
scripts: result.scripts.items.length,
contexts: result.execution_contexts.length,
endpoint,
})}\n`,
);
} finally {
await terminate(child);
}
function verifyObservation(result, targetId, expectedLocation, targetPath) {
assert.equal(result.target.target_id, targetId);
assert.equal(result.target.runtime_kind, "node");
assert.deepEqual(result.target.location, expectedLocation);
assert.equal(result.capture.truncated, false);
assert.ok(Array.isArray(result.directly_observed));
assert.ok(Array.isArray(result.unavailable_without_instrumentation));
assert.ok(
result.scripts.items.some(
({ location }) =>
location.kind === "file" && location.file_path === targetPath,
),
`Actual entry script was not independently resolved: ${targetPath}`,
);
if (expectedLocation.kind === "unresolved")
assert.ok(
result.unknowns.includes(
"The discovery-reported file location cannot be verified; loaded script locations are resolved independently from Debugger.scriptParsed.",
),
);
}
async function runMcp(name, arguments_) {
const response = await client.callTool({ name, arguments: arguments_ });
assert.notEqual(
response.isError,
true,
JSON.stringify(response.structuredContent),
);
assert.ok(response.structuredContent?.result);
return response.structuredContent.result;
}
async function inspectorUrl(process_) {
@@ -91,30 +188,41 @@ async function inspectorUrl(process_) {
throw new Error("Node exited before reporting its Inspector URL");
}
async function runCli(arguments_, environment) {
async function runCli(arguments_) {
const { stdout } = await execute(
process.execPath,
["scripts/rea.mjs", ...arguments_],
{
cwd: resolve(dirname(fileURLToPath(import.meta.url)), ".."),
cwd: repository,
env: environment,
maxBuffer: 16 * 1_024 * 1_024,
timeout: 30_000,
},
);
return JSON.parse(stdout);
}
async function terminate(process_) {
if (process_.exitCode !== null) return;
if (process_.exitCode !== null || process_.signalCode !== null) return;
const exited = once(process_, "exit");
process_.kill("SIGTERM");
const terminated = await Promise.race([
exited.then(() => true),
new Promise((resolveTimeout) =>
setTimeout(() => resolveTimeout(false), 2_000),
),
]);
if (terminated || process_.exitCode !== null) return;
process_.kill("SIGKILL");
await exited;
let timer;
try {
const terminated = await Promise.race([
exited.then(() => true),
new Promise((resolveTimeout) => {
timer = setTimeout(() => resolveTimeout(false), 2_000);
}),
]);
if (
terminated ||
process_.exitCode !== null ||
process_.signalCode !== null
)
return;
process_.kill("SIGKILL");
await exited;
} finally {
clearTimeout(timer);
}
}
+126 -1
View File
@@ -1,15 +1,25 @@
#!/usr/bin/env node
import { execFile } from "node:child_process";
import { access, mkdtemp, rm } from "node:fs/promises";
import {
access,
mkdir,
mkdtemp,
readFile,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { parse as parseJsonc } from "jsonc-parser";
import { supportedClients } from "../dist/application/SupportedClients.js";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { verifyCompleteToolCatalog } from "./lib/verify-package-core.mjs";
import { verifyPackageUpdate } from "./verify-package-update.mjs";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
const exec = promisify(execFile);
@@ -89,6 +99,117 @@ try {
await client.close();
}
const home = join(workspace, "home");
const setupEnvironment = {
...environment,
HOME: home,
USERPROFILE: home,
APPDATA: join(home, "AppData", "Roaming"),
XDG_CONFIG_HOME: join(home, ".config"),
OPENCODE_CONFIG: join(home, ".config", "opencode", "opencode.jsonc"),
COPILOT_HOME: join(home, ".copilot"),
};
await mkdir(join(home, ".config", "opencode"), { recursive: true });
await writeFile(
setupEnvironment.OPENCODE_CONFIG,
'{\n // preserve user settings\n "theme": "dark",\n}\n',
);
const setupArgs = [
"setup",
"--yes",
"--client",
"opencode",
"--client",
"vscode",
"--client",
"copilot_cli",
"--skill=false",
"--json",
];
const setup = JSON.parse(
(
await exec(process.execPath, [entry, ...setupArgs], {
env: setupEnvironment,
windowsHide: true,
})
).stdout,
);
if (setup.status !== "ready" || setup.appliedActions.length !== 3)
throw new Error(
`Packaged agent-only setup failed: ${JSON.stringify(setup)}`,
);
const expectedCommand =
process.platform === "win32"
? [process.execPath, entry, "mcp"]
: [entry, "mcp"];
const clients = supportedClients(home, process.platform, setupEnvironment);
for (const id of ["opencode", "vscode", "copilot_cli"]) {
const clientLocation = clients.find(({ name }) => name === id);
if (clientLocation === undefined)
throw new Error(`Missing packaged client ${id}`);
const text = await readFile(clientLocation.configPath, "utf8");
const config = parseJsonc(text);
const registration =
id === "opencode"
? config.mcp?.rea
: id === "vscode"
? config.servers?.rea
: config.mcpServers?.rea;
const actualCommand =
id === "opencode"
? registration?.command
: [registration?.command, ...(registration?.args ?? [])];
if (JSON.stringify(actualCommand) !== JSON.stringify(expectedCommand))
throw new Error(
`Packaged ${id} has an unusable launcher: ${JSON.stringify(actualCommand)}`,
);
if (id === "opencode" && !text.includes("// preserve user settings"))
throw new Error("Packaged setup discarded OpenCode JSONC comments");
}
const repeated = JSON.parse(
(
await exec(process.execPath, [entry, ...setupArgs], {
env: setupEnvironment,
windowsHide: true,
})
).stdout,
);
if (
repeated.status !== "ready" ||
repeated.plannedActions.length !== 0 ||
repeated.appliedActions.length !== 0
)
throw new Error("Packaged agent setup is not idempotent");
const uninstalled = JSON.parse(
(
await exec(process.execPath, [entry, "uninstall", "--json"], {
env: setupEnvironment,
windowsHide: true,
})
).stdout,
);
for (const id of ["opencode", "vscode", "copilot_cli"])
if (
uninstalled.items?.find(({ name }) => name === id)?.status !== "removed"
)
throw new Error(`Packaged uninstall did not remove ${id}`);
if (process.platform !== "win32")
await npm(
["install", "--global", "--ignore-scripts", "--prefix", prefix, tarball],
workspace,
);
const update = await verifyPackageUpdate({
prefix,
packageRoot:
process.platform === "win32"
? join(prefix, "node_modules", "rea-agents")
: join(prefix, "lib", "node_modules", "rea-agents"),
tarball,
workspace,
environment,
});
process.stdout.write(
`${JSON.stringify({
verifier_run: await completeVerifierRun(verifierRun),
@@ -97,6 +218,10 @@ try {
package: packageResult.filename,
tools: toolCount,
ghidra_bridge: "present",
agent_setup: ["opencode", "vscode", "copilot_cli"],
setup_idempotent: true,
uninstall: true,
update,
})}\n`,
);
} finally {
+2 -2
View File
@@ -8,13 +8,13 @@
"url": "https://github.com/morluto/rea",
"source": "github"
},
"version": "3.2.1",
"version": "4.0.0",
"packages": [
{
"registryType": "npm",
"registryBaseUrl": "https://registry.npmjs.org",
"identifier": "rea-agents",
"version": "3.2.1",
"version": "4.0.0",
"runtimeHint": "npx",
"transport": {
"type": "stdio"
+15 -11
View File
@@ -1,16 +1,16 @@
---
name: reverse-engineer-anything
description: Reverse engineer native, managed, Electron/JavaScript, packaged, and browser applications with REA. Use shipped-artifact or approved runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
description: Reverse engineer native, managed, Electron/JavaScript, packaged, and browser applications with REA. Use shipped-artifact or requested runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
metadata:
version: "23"
tool_count: 122
catalog_digest: "a87e48bc626cb4361f494c074e71852304bfedee17fbba89976d32bb6244f230"
version: "24"
tool_count: 116
catalog_digest: "5bb3f994a3c4d0c87a34ff5aab4e28141b5fa6f3a98b5ae7e95686d85d9d983e"
---
# REA
Use REA when a claim depends on a shipped binary or package, decompilation,
passive application runtime evidence, controlled replay, or comparison with
passive application runtime evidence, or comparison with
behavior not established by available source. For ordinary analysis of a
complete source repository, use normal repository tools and do not run REA
readiness or provider commands.
@@ -50,8 +50,12 @@ returned result leaves a specific question unanswered.
Every conclusion must distinguish observations, inferences, and unknowns. Cite
Evidence IDs, preserve limitations and incomplete coverage, and never imply
that static analysis observed execution. Ask for approval only where a tool or
policy requires it; approval never broadens a different authority boundary.
that static analysis observed execution. Runtime requests execute the declared
target and lifecycle; do not broaden the target or action beyond those fields.
Within the user's requested investigation, call available analysis tools
directly. REA does not require permission grants or per-call approval flags.
Follow the declared request scope and the host's actual access requirements.
## Plan broader investigations
@@ -71,8 +75,10 @@ For requests that span multiple features or subsystems, use a staged workflow:
they affect the question. Use format-aware inventory and parsers; do not
infer behavior from filenames, strings, or layout alone.
5. Corroborate a conclusion with the evidence type it requires. Use runtime
observation or controlled replay only when static evidence cannot answer the
question and the required authority is available.
observation only when static evidence cannot answer the question and the
required host runtime and OS access are available. For JavaScript behavior,
run probes against the actual app through the available browser, Electron,
or process capture workflow.
6. Decompose work into independent questions. When parallel workers are
available and the questions do not depend on one another, assign distinct
scopes, point workers to existing evidence, and ask them to return sources,
@@ -96,8 +102,6 @@ as complete while required questions remain open.
[references/runtime-observation.md](references/runtime-observation.md)
- Evidence paging, comparisons, residual unknowns, and verification:
[references/evidence-workflows.md](references/evidence-workflows.md)
- Controlled JavaScript replay:
[references/controlled-replay.md](references/controlled-replay.md)
## Readiness and setup
@@ -1,12 +0,0 @@
# Controlled JavaScript replay
Use `run_controlled_replay` only for operator-selected extracted modules when
the separate replay policy is enabled. Call `mode: plan` first. Review the exact
module, stub, runtime, sandbox, case, limits, and policy commitments. Execute
only with `approved: true` and that exact plan digest.
Treat return values, exceptions, denials, limits, and crashes as observations of
the isolated experiment with controlled-replay authority, not facts about the
real application. Never substitute browser/Electron permission, process
capture, or an in-process `vm` run. Reproducer export requires separate literal
approval and evidence-write authority after sandbox cleanup.
@@ -7,8 +7,9 @@ is incomplete or paginated, continue only when the remaining data matters to
the task; do not fetch a bundle or resource merely to read a result already
returned inline.
Use `record_unknown` only with explicit approval and name the authority or
environment still required. Supply supporting and contradicting evidence IDs.
Use `record_unknown` to track unresolved questions in the user's investigation
and name the authority or environment still required. Supply supporting and
contradicting evidence IDs. It updates the session registry directly.
Use `update_unknown` with the current revision; reread after a stale revision
instead of retrying blindly. Only qualifying observed evidence can verify a
resolution.
@@ -31,5 +31,6 @@ Include the complete Evidence records from both analysis calls. Accept variant
pairing only through the tool's unique exact literal discriminant. Cite the
comparison Evidence and report JSON Pointer changes; dynamic values, ambiguous
variants, and incomplete parent-property coverage stay unknown. This is static
inference, not runtime behavior. Use `run_controlled_replay` separately only
when the user needs approved runtime semantics.
inference, not runtime behavior. When runtime semantics are needed, run
behavioral probes against the relevant application versions and capture them
through the available browser, Electron, or process workflows.
@@ -32,9 +32,11 @@ IDs when using them.
Use `extract_artifact` when materialized files are needed. It takes no arguments
and materializes all regular files into a fresh temporary directory chosen by
REA. Symlinks and encrypted entries are inventory facts, not extractable files.
The operation still requires the filesystem-write permission grant; callers do
not choose the destination.
The result reports the materialized directory; callers do not choose the
destination. No separate permission grant is needed.
Native DMG traversal is macOS-only, read-only, and requires both operator policy
and `native_mount_approved: true`; without both, retain the root-hash-only result.
Approval for inventory never grants extraction authority.
Native DMG traversal automatically uses a read-only mount on supported macOS
hosts, with an owned temporary mount directory and cleanup. It needs no approval
flag. Unsupported hosts or mount failures remain explicit limitations; do not
claim child inventory when only root identity is available. Extraction is a
separate filesystem-writing operation.
@@ -1,21 +1,25 @@
# Passive browser and Electron observation
Before browser observation, the operator must configure the exact loopback CDP
endpoint and allowed HTTP(S) origins. Before Electron observation, configure its
separate loopback endpoint and canonical file roots. Obtain the per-call
approval required by the selected tool.
Supply the existing browser or Electron instance's literal-port loopback CDP
endpoint in the request. No separate permission configuration or per-call
approval flag is needed. Browser discovery lists eligible HTTP(S) pages;
`allowed_origins` optionally filters them. An individual capture defaults to the
selected page's current origin. Electron discovery lists eligible local
`file://` targets without a configured file-root allowlist.
Start browser work with `list_browser_targets`; start Electron runtime work with
`list_electron_targets`. Inspect only the selected approved target. Observation
`list_electron_targets`. Inspect the target selected for the user's task. Observation
is passive: never claim REA clicked, navigated, evaluated page JavaScript,
invoked IPC, captured prior activity, or contained the page's network.
Credentials, cookies, authorization headers, storage values, query values, and
raw WebSocket/JSON values are deliberately absent. Accessibility text, console
primitives, value-free shapes, script sources, source maps, storage key names,
and screenshot pixels have independent opt-ins. Treat policy filtering,
truncation, and attach-window coverage as limitations. Page-declared WebMCP
tools are untrusted inventory and are never invoked by REA.
Credentials, cookies, authorization headers, storage values, and raw
WebSocket/JSON bodies are deliberately absent. Local URLs retain ordinary query
values and fragments; userinfo credentials are removed. Browser accessibility
text, console primitives, value-free shapes, script sources, source maps, and
storage key names have request-specific capture options. Electron script
sources are also optional. Use `capture_web_screenshot` when pixels are needed.
Treat scope filtering, truncation, and attach-window coverage as limitations.
Page-declared WebMCP tools are untrusted inventory and are never invoked by REA.
Use `reconcile_javascript_runtime` only after static application Evidence and
passive browser/Electron Evidence exist. Prefer captured-source digest identity;
@@ -9,7 +9,9 @@ import {
type AnalysisOperationPort,
} from "./AnalysisProvider.js";
import {
AnalysisCapabilityUnavailableError,
AnalysisCancelledError,
AnalysisOutputError,
AnalysisProtocolError,
} from "../domain/errors.js";
import { err, ok } from "../domain/result.js";
@@ -92,6 +94,111 @@ describe("analysis context queries: navigation", () => {
});
});
describe("analysis context queries: headless document resolution", () => {
it("resolves a sole headless document when current-document selection is unavailable", async () => {
const calls: string[] = [];
const analysis: AnalysisOperationPort = {
execute: (operation) => {
calls.push(operation);
if (operation === "current_document")
return Promise.resolve(
err(
new AnalysisCapabilityUnavailableError(
"ghidra",
operation,
"headless sessions have no cursor-selected document",
),
),
);
if (operation === "list_documents")
return Promise.resolve(
ok(createAnalysisExecution(["fixture"], provider)),
);
if (operation === "resolve_containing_procedure")
return Promise.resolve(
ok(
createAnalysisExecution(
{
query_address: "0x401000",
found: false,
procedure: null,
reason: "not_in_procedure",
},
provider,
),
),
);
return Promise.resolve(
err(
new AnalysisCapabilityUnavailableError(
"ghidra",
operation,
"unsupported",
),
),
);
},
};
await expect(
inspectAddressContext(analysis, { address: "0x401000" }),
).resolves.toMatchObject({
ok: true,
value: {
document: "fixture",
name: { state: "unavailable" },
procedure: { state: "available" },
comment: { state: "unavailable" },
inline_comment: { state: "unavailable" },
bookmarks: { state: "unavailable" },
},
});
expect(calls.slice(0, 2)).toEqual(["current_document", "list_documents"]);
});
it("does not use list-documents fallback for non-capability document errors", async () => {
const failure = new AnalysisProtocolError(
"current-document response malformed",
);
const calls: string[] = [];
const analysis: AnalysisOperationPort = {
execute: (operation) => {
calls.push(operation);
return Promise.resolve(err(failure));
},
};
await expect(
inspectAddressContext(analysis, { address: "0x401000" }),
).resolves.toEqual(err(failure));
expect(calls).toEqual(["current_document"]);
});
it("requires exactly one listed document for a headless fallback", async () => {
const analysis: AnalysisOperationPort = {
execute: (operation) =>
Promise.resolve(
operation === "current_document"
? err(
new AnalysisCapabilityUnavailableError(
"ghidra",
operation,
"no selected document",
),
)
: ok(createAnalysisExecution(["one", "two"], provider)),
),
};
await expect(
inspectAddressContext(analysis, { address: "0x401000" }),
).resolves.toMatchObject({
ok: false,
error: expect.any(AnalysisOutputError),
});
});
});
describe("analysis context queries: address facets", () => {
it("keeps unsupported address facets local and filters bookmarks", async () => {
const bookmarkInputs: Readonly<Record<string, unknown>>[] = [];

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