Compare commits

...
Author SHA1 Message Date
morluto a2955d6607 fix(replay): pin the replay timezone and map undefined to null 2026-10-05 16:46:41 +08:00
morlutoandGitHub 333d118c0f fix(javascript): classify assignment targets and retain compound-assignment reads (#537) 2026-10-05 16:16:57 +08:00
morlutoandGitHub 4c8dc39f43 fix(javascript): honor computed keys when naming callables, exports, requests and options (#536) 2026-10-05 15:52:06 +08:00
morlutoandGitHub d40502aa3b fix(native): match compound otool keys exactly and badge unknown thread entrypoints (#534) 2026-10-05 15:43:20 +08:00
N0zoM1z0andGitHub 4007b20fc2 fix(mcp): emit valid schemas for empty arrays (#535)
Replace empty tuples with a shared empty-array schema. Validate all input
and output schemas against Draft 2020-12 during catalog generation and
packaged MCP verification, with runtime and catalog regression coverage.

Closes #528

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

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

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

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

---------

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

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

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

---------

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

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

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

---------

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

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

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

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

---------

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

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

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

---------

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

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

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

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

---------

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

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

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

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

---------

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

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

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

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

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

---------

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

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

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

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

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

---------

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

* test(hopper): isolate real inventory verification

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

* fix: preserve address spaces in call path evidence

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

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

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

* test: prioritize real artifact E2E and captured golden regressions

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

* test: prune redundant cases and strengthen boundary assertions

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

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

* fix(catalog): separate portable metadata from native host availability
2026-10-04 16:02:36 +08:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
0356f7677f chore(main): release rea-agents 3.2.1 (#499)
* chore(main): release rea-agents 3.2.1

* chore: normalize release product catalog

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-04 03:58:16 +08:00
morluto b7bc855492 test(release): accept guarded npm publishing 2026-10-04 03:52:44 +08:00
morluto 726b730b84 fix(release): tolerate npm registry propagation delay 2026-10-04 03:48:35 +08:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
b7539467a6 chore(main): release rea-agents 3.2.0 (#472)
* chore(main): release rea-agents 3.2.0

* chore: normalize release product catalog

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-04 00:43:14 +08:00
morlutoandGitHub 7ee3f236a9 test: replace implementation lock-in with behavioural assertions (#498)
* fix(process): normalize reactive rendered-frame timestamps

A reactive `resize` action stamped `rendered_frames[].at_ms` with the raw
elapsed time while every other rendered frame used
`normalizeProcessElapsedTime`. Under the default 10ms time bucket the next
frame buckets down and appears to move backwards, so `parseProcessCapture`
rejected the entire capture with "rendered_frames: timestamps must be
ordered". No test combined a reactive resize with time bucketing, so the
path was never exercised.

Thread the scenario's bucket into the reactive effect host and normalize the
renderer call, matching what the scheduled-event resize path already did.
`dispatched_at_ms` and `captured_at_ms` stay raw on both paths, so this
changes only the one inconsistent clock.

Also replace the wall-clock-scheduled interactive PTY scenario with
terminal_text triggers. A fixed `at_ms` could fire before the child wrote
its prompt under host contention, dropping the input and the resize echo.
That rewrite is what surfaced this bug, and it now passes deterministically.

Replace the 11 `if (!capability.available) return;` guards across the PTY
suite with a shared it.skipIf gate. The old form reported a green pass with
zero assertions on any host lacking native PTY authority; skips are now
named and reported.

Verified by reverting only the normalization, which reproduces the
validation failure immediately.

* fix(install): add recovery guidance to version-mismatch failure

Every installer failure except one told the user what to do next. The
version-mismatch failure reported only that the installed version did not
match, with no remedy, which contradicts the requirement to keep
diagnostics actionable for mismatch locations.

Add a remedy in the same style as the neighbouring failures and drop the
exemption the test carried for it, so all ten installer failures now assert
they carry a recovery action.

* ci: repair stale curated Windows lane and unblind the duplication gate

The `windows-curated` job listed fifteen test files and nine of them no
longer existed, including six `processOwnership*` files moved to
`src/process/` in 4ecc7a72. Vitest silently drops unmatched paths, so the
job ran six files and reported success. Repoint the paths at their current
locations and add a test that fails when any listed path is renamed or
removed, so the lane cannot silently lose coverage again.

The duplication gate ignored `**/*.test.ts`, so it reported zero clones
across the test tree while running on every PR. Include test files, remove
the two largest duplicated clusters, and ratchet the threshold to the
measured result so the gate can no longer pass by not looking.

Run local tests with two workers instead of one. A single worker made a
full local run cost ~140s of wall time at ~100% CPU on a 10-core host,
which was the largest dev-cycle cost in the repository. Local now matches
the concurrency CI already proves green, and PTY scenarios moved to a
serial project because they contend for host terminal resources. Full local
suite: 140s to ~89s.

Regenerate the managed conformance manifest. Commit 4ecc7a72 edited
SKILL.md without refreshing the digest, so `npm run check:pr` has been
failing on main since that commit.

* test: replace implementation lock-in with behavioural assertions

Audit the suite for tests that cost maintenance without catching defects,
then remove the lock-in rather than the coverage.

Replace assertions that froze incidental detail:

- toolContracts.test.ts pinned group sizes and 50 lines of literal tool
  names, so any additive catalog change required a test edit. Replace with
  four structural invariants: global name uniqueness, group containment in
  the published inventory, group disjointness, and non-empty groups. Both
  invariants are mutation-checked by orphaning and duplicating a tool.
- Six tests asserted only that a schema accepts its own canonical fixture.
  Replace five with real rejections and two with property tests covering
  the invariants those tests were gesturing at: `isDbSaveComplete` is
  downward closed in every expected count, and frame retention holds at
  any size.
- A concurrency test whose fake returned `? [] : []` could not fail. Give
  the fake real shared state so a lost update is observable.
- providerProcess.test.ts asserted listener and timer counts plus exact
  cleanup retry counts. Assert detachment behaviour instead, so an
  unrelated internal timer no longer breaks the test.
- Scale tests that rebuilt removed caps: 16 MiB and 10,001 files to prove
  limits that no longer exist, 100,001 frames compared against their own
  length, and a projection asserting a literal 10,201 bridge candidates.
  Replace with derived relationships that hold at any size.
- installScript.test.ts merged two tables and now asserts alongside the
  exact wording that every failure carries a recovery action.
- cdpBrowserProvider.sensitive_data asserted outcomes produced after the
  observation window closes while using a zero-length window; give it a
  real window so it measures the malformed-body rejection rather than host
  speed.
- runtime.test.ts repeated the catalog assertion from the MCP boundary
  suite while spawning a second stdio server. Give the database-kind
  scenario its own claim and make stale-build failures legible.

Extract two byte-identical fixture preambles that the max-lines rule had
split across five and three files respectively, removing ~1,000 duplicated
lines.

Profile the heaviest files rather than only running them: the Apple
projection test drops from 6.4s to 0.28s and the reference source import
from 2.3s to 0.2s, with stronger assertions in both cases.

* ci: keep native-authority suites out of the curated Windows lane

CI showed the repointed curated list was red. Three of the restored files
require verified Windows native authority, which windows-latest does not
provide by design:

- src/ghidra/GhidraProvider.test.ts, the replacement for the deleted
  tests/boundary/providers/ghidra/ghidraProvider.test.ts
- tests/boundary/providers/ghidra/ghidraClient.test.ts
- tests/boundary/process/providerProcess.test.ts

All three fail with PrivateRuntimeRootUnavailableError, because Job Object
process ownership and private-runtime DACL enforcement are unimplemented.
The previous ghidraClient and providerProcess entries were added in this
branch and never belonged here; the GhidraProvider replacement does not
inherit the deleted file's Windows behaviour.

Those claims belong behind the same approval boundary as the real Ghidra
Windows workflow rather than the default lane. Record the exclusion in the
guard test so the lane cannot regress to red, and in ci.yml so the next
reader knows the omission is deliberate.

The lane now runs 14 files, up from the 6 it silently ran before, all
verified green on windows-latest.

* test: stop max-lines from forcing test files to split

The 500-line cap on test files only ever produced duplication. It chopped one
binary session suite across nine files whose names were truncated
mid-sentence, and it is why the curated Windows lane referenced six
`processOwnership*` paths that had been split apart in the first place.

Turn the cap off for tests and fixtures, keeping the function-length limit,
then merge the nine binary session files back into one suite. That restores
all 22 cases under a single module registry instead of nine and cuts the
composition project from 20 files to 12. The merged cases become nine
top-level describes grouped by concern, because a single wrapping describe
would itself breach the function-length limit.

Drop applicationWorkflowVerifier.test.ts. Its first case asserted
`verified: true`, which the script emits as a literal after its own checks,
so it could not fail, and every remaining assertion was `expect.any`. Its
second case asserted an export-shape comparison that
applicationWorkflowCli.test.ts already asserts through the product CLI,
which is the stronger claim.

Finally, five javascriptSemanticAnalysis files imported `it` from
@fast-check/vitest while containing no properties. Import `it` from vitest
so those files no longer read as property-based coverage.

* test: stop creating temp scaffolding the temp helper immediately deletes

`createTestTempDirectory` called `createTestWorkspace`, which creates `home`
and `xdg/{config,cache}`, then deleted two of those three directories so the
caller would receive an empty root. Every caller paid three `mkdir` calls and
two `rm` calls per temporary directory for scaffolding it never used, across
roughly a hundred test files.

Create the directory directly instead. Callers that genuinely need a home or
XDG tree can call `createTestWorkspace`.

* test: merge the split process reactive runtime suite

Five files shared one describe name in pairs and re-imported the same fixture
helpers. With the shared preamble already extracted into
processReactiveRuntime.fixture.ts, merge them into one suite of five
top-level describes grouped by concern. All 12 cases are preserved and the
domain project drops from 92 files to 88.

* test: drop two identity round-trips from node runtime characterization

One test built a plan with `max_calls: 10_001` and asserted the field came
back as 10_001, which asserts that the plan constructor copies a field the
test just set, and freezes a former ceiling in the process.

The other built a 201-character alias and asserted it parsed back to the
same string, which round-trips a value the test created.

Neither could fail for a reason that matters. The remaining cases still
cover preparation, stale-plan rejection, single dispatch, and artifact write.

* test: hoist duplicated graph fixtures into the shared fixture module

Eleven copies of three helpers were spread across the seven
javascriptApplicationGraph files: `completeCoverage` five times,
`nodeByLabel` four times, and `firstOf` twice, all byte-identical. The
fixture module already carried a private `completeCoverage`.

Export all three from javascriptApplicationGraph.fixture.ts and import them,
removing 95 duplicated lines.

* test: separate the reactive fixture's two PTY writes

The multi-source run resolved target_lost instead of passed intermittently.
The ready transition matches terminal text "Ready" and consumes that
observation, advancing the terminal frontier; the collecting state then
needs "Collecting" in a later observation. The fixture wrote both back to
back, so the PTY sometimes coalesced them into one chunk and the collecting
trigger could never match.

Move the "Collecting" write after the synchronous shim probe. The probe takes
real wall time, so the reader has drained the first chunk before the second
is written, which makes the ordering deterministic rather than timing
dependent.

Measured 33/34 isolated passes, against roughly even odds before. Under four
concurrent copies it still failed 2 of 12, so this narrows the window rather
than closing it: the underlying race is that node-pty delivers buffered
output through a callback the exit path never awaits, so the harness can
declare the target lost before the last chunk is ingested. The test is left
strict because it is correctly detecting that.

* test: replace expect.any placeholders with real results

Four acceptance scenarios and one composition scenario asserted only that a
field was a string, number, object, or array. Those assertions cannot fail
for a reason that matters, and three of them sit behind real CLI process
spawns that cost about 1.5s.

Capture the actual results once and assert them exactly. The CLI parity
scenario now pins the trace summary and coverage, the version comparison
summary and coverage, and the source-to-bundle summary and coverage, so each
spawn earns its cost. The MCP rejection case now asserts `invalid_request`
with category `invalid_input`, which is what makes it a caller-input failure
rather than a provider one.

The artifact comparison case asserted three `expect.any` values immediately
before an assertion that subsumed them. Replace them with the claim that
actually matters above 500 entries: manifest counts must reconcile with the
arrays returned, and every written file must appear as an occurrence, so a
reintroduced truncation would fail.

* test: remove impossible and incidental prompt assertions

Two assertions in toolResult could never fail: each asserted that the
serialized result did not contain a string that appears nowhere in its own
inputs, so only in the assertion itself.

promptContracts asserted that a `Set` built from a locally declared six
element literal had no duplicates, and pinned thirteen incidental fragments
of rendered prompt prose. Drop the tautology and reduce the prose block to
the five properties an analyst actually depends on: caller input framed as
data rather than instruction, tools offered as options rather than a
mandated sequence, the epistemic vocabulary present, tools never framed as
authorization, and deterministic suggestion order.

* test: hoist the remaining duplicated graph fixtures

Eight more byte-identical helper copies across the javascriptApplicationGraph
files: `graphForNode` four times, `unknownEvidence` twice, and `contentNode`
twice. Move them into javascriptApplicationGraph.fixture.ts alongside the
three hoisted earlier, and drop the imports they left behind.

Net -92 lines across the seven files.

* test: drop enhancedTools helpers already in the shared harness

Three of the six enhancedTools files still carried local copies of
PROCEDURES, inventory, and emptyBounded that are byte-identical to the
exports added to enhancedToolsHarness.ts. Import them instead.

Only fixturePort, connect, and jsonResult differ per file, so those stay
local.

* test: replace repeated process-tree event literals with fixture builders

The file spelled out nineteen thirteen-field event literals and thirteen-field
process nodes inline, varying only three or four fields each, so a test read as
a wall of repeated boilerplate rather than the transitions under test.

Add eventProcessTree.fixture.ts with spawn, exit, re-exec and reparent
builders plus a process-node builder, and express each case as the sequence it
describes. The test drops from 422 to 214 lines with all nine cases intact.

* test: share the process-ownership validation fixtures

The two process-ownership validation files each declared byte-identical copies
of an `ownership` constant and a 24-line `host` builder. Move both into
ProcessOwnership.fixture.ts and import them.

* test: make rejection and retention assertions exact

The reactive scenario rejection table asserted only that each of its six
fixtures failed to parse. A typo anywhere in a fixture would satisfy that,
including one that invalidated an unrelated field. Record the field each row
is meant to invalidate and require the reported issue to point at that path
with the expected code. Verified by making one row valid except for an
unknown key: the row now fails instead of passing for the wrong reason.

The large-body replay test asserted that the serialized result exceeded five
million characters as a proxy for the bodies being retained, which escaping
or padding could satisfy even under truncation. Compare each retained body to
the input byte for byte and total the retained length. Verified by shaving one
byte off the expected value, which now fails.

* test: share the elicitation grants fixtures

The three processCaptureElicitationGrants files each declared the same five
helpers inline: an elicitation request, a fixed clock, a ceiling-only
permission authority, a request-context builder, and a verified-state stub.
Move them into elicitationGrantsHarness.ts and import them.

* fix(process): wait for trailing PTY chunks before declaring target loss

The exit path drained the reactive coordinator and then submitted
target_lost, but node-pty delivers buffered output through onData callbacks
that run outside the coordinator's queue. A trailing chunk that had not been
delivered yet produced no journal entry, so the drain had nothing to wait for
and the target was declared lost while a terminal trigger was still
satisfiable. That is what made the multi-source reactive fixture resolve
target_lost instead of passed at roughly even odds.

Yield to the event loop until the capture journal stops growing before
draining, bounded so a pathological stream cannot stall exit.

The fixture change that accompanied this is no longer carrying the weight on
its own; the ordering it introduced remains correct and worth keeping.

Measured 18 consecutive passes across isolated and four-way concurrent runs,
against roughly even odds beforehand. A control run with the wait disabled did
not reproduce the failure, so the machine was not loaded enough for a
controlled comparison; the before/after numbers are from separate sessions.

* test: hoist the last duplicated domain and coordinator fixtures

processTraceComparison and its Part2 declared the same eight helpers, and the
two ProcessReactiveCoordinatorFailures files declared the same three. All were
byte-identical. Move them into shared fixture modules and import them, netting
429 fewer lines.

The earlier attempts at this failed because depth-based scanning mis-detected
helper boundaries: bracket characters inside type annotations, such as
`options: { truncated?: boolean }`, unbalance the count. Extracting by verified
line range instead worked first time, and unused imports were then settled by
alternating typecheck and lint rather than by guessing which names a body uses.

* ci: ratchet the duplication gate to the reduced baseline

Test-file duplication is down to 2.58% of lines across src and tests, from
3.39% when tests were first included in the gate. Lower the threshold from
3.2 to 2.7 so the reduction cannot regress.

* test: merge the remaining artificially split test files

Five suites were each split across two or three files by the max-lines rule
that is no longer applied to tests, and every split produced a duplicate
describe name and a duplicated import block.

Merge them back: processTraceComparison, ProcessReactiveCoordinatorFailures,
ProcessOwnership.validation, processCaptureElicitationGrants, and
processCaptureElicitation. Net -62 lines and five fewer files.

Give each merged describe a name describing what it covers, since the splits
were arbitrary and the names now collide. Test bodies are unchanged; every
describe already fitted the function-length limit, so merging them as
separate top-level blocks satisfies it.

Imports were settled by writing each block explicitly after typecheck and lint
disagreed. Lint is right about names that appear only inside string literals,
such as `values(["terminal", "process", "http", "websocket"])`, while
typecheck is right about names a body genuinely references, so neither alone
was sufficient.

* test: merge the graph suite and share the enhancedTools result helper

The seven javascriptApplicationGraph files were one suite with one describe
name repeated seven times. Merge them into a single file with seven
describes named for what each covers, keeping all 18 cases.

Three of the enhancedTools files declared byte-identical copies of a
jsonResult helper. Move it into enhancedToolsHarness.ts alongside the rest of
the shared fixtures.

The remaining enhancedTools files stay split: their fixturePort helpers are
three genuinely different provider doubles rather than duplicates, so merging
them would mean renaming per-file seams and lose the reason each exists.

* ci: repoint the Windows lane at the merged ownership validation file

Merging the split ProcessOwnership.validation files removed the two Part
paths the curated Windows lane referenced. The curated-lane guard test caught
it on CI, which is the failure it was added to prevent.

* test(process): make lifecycle assertions falsifiable and scope them to POSIX

Two assertions could not fail, so neither observed the behaviour it claimed.

The deadline disposal test aborted an already-aborted controller and then read
an immutable interruption, so it passed whether or not disposal released the
timer or the listener. Dispose before aborting instead and assert the owned
timer count reaches zero, which distinguishes a released deadline from a
retained one. Verified by mutation: emptying dispose, dropping the listener
removal, and dropping the timer clear each fail the test.

The supervisor test asserted that a repeated stop still resolved, but stop
caches its promise, so it proved nothing about detachment. Replace it with a
late error and close event after disposal and assert the diagnostic channel
stays untouched, keeping an attached control that proves the channel works.
Verified by mutation: retaining the error listener fails the test.

Also mark the TERM-to-KILL escalation POSIX-only. libuv maps every signal to
TerminateProcess on Windows, where a child that ignores SIGTERM cannot survive
the TERM step, so the escalation and SIGKILL signal code are unreachable there.

* fix(vitest): restore cross-project scheduling for local runs

Dropping the per-project `sequence.groupOrder` made every project run
concurrently. `fileParallelism: false` only serialises files inside a single
project, so `process-boundary` could still overlap with the boundary projects
that observe host process and terminal state, which is the contention that
drops input and resize echoes. The removed blanket per-project serialisation
was slower, but it was never what isolated projects from each other.

Keep the two-worker intra-project parallelism that made local runs fast and
apply the ordering locally only, so CI keeps the concurrency it proves green.

* fix(acceptance): make the database startup record check observable

The check asserted stderr contained no fatal record, but a database-kind
startup that only lists the catalog starts no bridge work and writes no
records at all, so the assertion passed without observing anything. Reading
stderr while the child was still running also raced the pipe, and splitting
the buffer could parse a partial trailing line.

Call a tool so startup records are actually produced, parse only complete
newline-delimited records, and read after the transport closes so delivery is
no longer in question. Guard the result with an explicit non-vacuity assertion
so the fatal check cannot silently stop observing anything.

Verified by mutation: injecting a level-60 record through the stderr logger
now fails the test.

* test(acceptance): restore the application workflow verifier coverage

Nothing else in the repository executes
scripts/verify-local-application-workflows.mjs, so deleting this file left the
only automated check on a shipped script. Regressions in its dist imports,
argument handling, or two-version and export-shape output could ship with a
green suite.

The two cases cost about 350ms and 500ms, so the duplication that motivated
removing them was never the reason to drop the coverage.

Verified by mutation: flipping the reported verification status fails the
suite.

* test(contracts): add a published tool name floor

Every remaining inventory invariant is structural, so deleting a contract from
both its group and the aggregate left the whole file green while the catalog
regenerated without it. Structural checks cannot detect removal; only a
declared baseline can.

Declare the 114 published names as a floor rather than an equality. Adding a
tool still needs no edit, while removing one now fails with a message naming
the tool. Verified by mutation: dropping a contract from its group so the
aggregate loses it too fails the floor.

* test: restore no-ceiling coverage above plausible truncation limits

Four suites claimed a result was complete at any scale while their own inputs
were bounded below where a truncation would appear, so the guarantee they
described was not actually under test. Scale independence cannot be asserted as
a relationship over a fixture smaller than the cap being ruled out.

Raise each fixture past any ceiling a regression would plausibly introduce,
which is far cheaper than the removed cases:
  referenceSourceImport  600 entries/64 KiB -> 5,000 entries with a 4 MiB member
  customProtocolCapture  generated max 40 frames -> 300..1,000
  appleApplication       12 frameworks/16 candidates -> 250/1,600
  nodeRuntimeCharacterization  assert the absent call-budget and alias bounds
    where they would be added, since the budget derives from the case count

Verified by mutation: a 1,000-entry import cap, a 500-frame capture cap, a
100-framework projection cap, a 100,000-call budget cap, and a 200-character
alias cap each fail the corresponding suite.

* fix(process): require a quiet period before declaring the target lost

Returning on the first unchanged journal sample granted a single event-loop
turn of grace. A chunk queued on a later iteration therefore still let
`target_lost` be submitted before the final terminal observation, discarding a
satisfiable reactive trigger under the same load-dependent exit scenario the
drain was added for, and `maxTicks` never bounded that because the loop exited
early every time.

Require several consecutive unchanged turns so a callback arriving on any of
the next few iterations still lands first, while keeping the overall tick bound
so a pathological stream cannot stall exit.

* fix: stop a 4 GiB fixture and measure the drain quiet period in time

The reference import fixture cycled its five sizes across all 5,000 entries,
which made a thousand 4 MiB members and nearly 4 GiB of fixture written
concurrently. It happened to fit locally but cannot fit a runner's disk. Cross
the two boundaries independently instead: entry count by volume of tiny
members, byte size by a single 4 MiB member, for about 4.2 MiB total. Verified
by mutation that a 1,000-entry cap and a 1 MiB per-member cap both still fail.

The drain check counted scheduler turns, but consecutive setImmediate turns
elapse in microseconds, so four of them finish long before a pty callback
delayed under host contention, which is when the trailing chunk matters.
Measure the quiet interval in elapsed time instead, so a late callback within
the window still lands before target loss. Verified by simulation: the
turn-count version missed any delay beyond a few microseconds, the timed
version observes delays up to the quiet interval.
2026-10-03 23:22:07 +08:00
morlutoandGitHub 9dd72c4911 docs: prioritize reusable analysis primitives (#493) 2026-10-02 06:33:38 +08:00
morlutoandGitHub 4ecc7a72f2 docs: generalize reverse-engineering workflow (#488) 2026-09-30 19:06:34 +08:00
morlutoandGitHub ed933d1893 feat(native): add investigation features (#486)
* feat(native): add UI and dispatch investigation workflows

* feat(ghidra): support macOS host validation

* docs: update native investigation capabilities

* feat(artifacts): decode compiled Interface Builder archives

* feat(ghidra): expose bounded p-code value flow

* test(ghidra): split host and cross-format verification

* fix(verify): follow complete result contracts

* refactor: remove unused authorization and schema helpers

* refactor: remove unused evidence result alias

* fix(ghidra): report jump-table evidence precisely

* fix(verify): align browser scenario fixture schema

* fix(setup): compare parsed client config structurally

* test(ghidra): verify AArch64 jump-table mappings

* fix(ghidra): recover AArch64 byte jump tables

* test(artifacts): verify compiled interface builder archives

* feat(native): trace UI actions through handlers

* fix(tests): update native trace and shutdown fixtures

* fix(verification): diagnose browser scenario failures

* fix(browser): wrap scenario snapshots in capture state

* fix(native): bind and bound investigation workflows

* fix(artifacts): preserve bounded interface builder evidence

* fix(ghidra): enforce admitted hosts and preflight tools

* fix(native): preserve trace cancellation and bounds

* test(application): expect bound analysis target evidence

* fix(native): preserve investigation bounds and cancellation

* fix(application): preserve bundle and unresolved trace evidence

* style(test): format artifact subject fixture
2026-09-30 00:41:05 +08:00
morluto fa26affc86 chore(server): remove unused result import 2026-09-29 19:24:05 +08:00
morluto 8097ec456a refactor(server): remove registration alias 2026-09-29 19:23:15 +08:00
morluto f570d40f50 refactor(errors): remove stale workspace failures 2026-09-29 19:23:08 +08:00
morluto bb6a58c2a7 refactor(server): use canonical elicitation type 2026-09-29 19:12:21 +08:00
morluto 6ee87fbf2d refactor(reference): share graph index projections 2026-09-29 19:12:15 +08:00
morluto a6d9e49ab4 refactor(browser): share canonical identity digest 2026-09-29 19:08:30 +08:00
morluto eec75ce918 refactor(browser): share authorized main-frame polling 2026-09-29 19:07:18 +08:00
morluto 4f4cc59873 refactor(domain): share AST property name reader 2026-09-29 19:07:05 +08:00
morluto dcb036292e test(cli): remove duplicate browser discovery case 2026-09-29 19:00:58 +08:00
morluto 9fca6c0296 refactor(replay): share sandbox probe arguments 2026-09-29 18:50:40 +08:00
morluto aa6eee1a71 refactor(artifacts): remove duplicate inventory wrapper 2026-09-29 18:50:29 +08:00
morluto c7e207c290 docs: clarify aggregate context tools 2026-09-29 18:42:57 +08:00
morluto b5c0b5b53e test(hopper): remove dossier identity assertion 2026-09-29 18:42:52 +08:00
morluto 4ec2343d91 test(replay): remove duplicate schema coverage 2026-09-29 18:42:48 +08:00
morluto 21241d2323 test(mcp): validate defaults at tool boundary 2026-09-29 18:42:42 +08:00
morluto 4593c29b64 refactor(contracts): remove duplicate schema aliases 2026-09-29 18:35:47 +08:00
morluto a3920e2abd refactor(evidence): remove obsolete ID resolvers 2026-09-29 18:30:19 +08:00
morluto 1b0ab75a84 refactor(permission): remove unused authorization helpers 2026-09-29 18:30:13 +08:00
morluto 3afcd64146 test(mcp): consolidate catalog inventory checks 2026-09-29 18:25:25 +08:00
morluto b783e7febe test: remove duplicate export schema acceptance 2026-09-29 18:16:11 +08:00
morluto 60c110d97b test(browser): remove stale input field assertion 2026-09-29 18:16:09 +08:00
morluto 9bd7e2cb45 test(cli): consolidate duplicate setup journey 2026-09-29 18:16:08 +08:00
morluto d6e545cc48 test: remove Vitest config test exports 2026-09-29 18:05:18 +08:00
morluto 9240bd8d16 test(hopper): name regex search assertion accurately 2026-09-29 18:00:53 +08:00
morluto ea853cebde test(conformance): remove fixture generator test option 2026-09-29 18:00:47 +08:00
morluto 9ddf22e196 test(browser): assert strict inputs without legacy fields 2026-09-29 18:00:39 +08:00
morluto 56e2bf7696 test: remove vacuous and duplicate assertions 2026-09-29 18:00:30 +08:00
morluto 41681742cc docs(tools): clarify complete inline workflows 2026-09-29 17:36:59 +08:00
morluto a5797c5be6 chore(ghidra): align supported release to 12.1.4 2026-09-29 17:36:46 +08:00
morluto 5f73cb061b refactor(tools): remove arbitrary workflow ceilings 2026-09-29 17:36:26 +08:00
morluto 6c68ff565f fix(process): capture complete host probe output 2026-09-29 17:36:04 +08:00
morluto dfad09e3b5 refactor(managed): derive inspection from PE CLI extents 2026-09-29 17:35:41 +08:00
morluto 3b64c813cf docs(tools): clarify complete inline results 2026-09-29 17:00:29 +08:00
morluto acf33d6df2 test: remove stale limits and handoff expectations 2026-09-29 17:00:16 +08:00
morluto ecf3ed8bdb refactor(tools): remove remaining arbitrary workflow caps 2026-09-29 17:00:06 +08:00
morluto 4ad937c6ca docs(tools): update inline workflow guidance 2026-09-29 16:29:34 +08:00
morluto 2f08c7c932 refactor(tools): remove artificial analysis ceilings 2026-09-29 16:29:23 +08:00
morlutoandGitHub 36aeec9586 docs: add REA tool design guidance (#485)
docs: clarify MCP tool design guidance
2026-09-29 16:28:13 +08:00
morluto 5dd3b4655e refactor(browser): remove arbitrary capture ceilings 2026-09-29 15:31:27 +08:00
morluto 4cea84d7cb docs(tools): describe uncapped tool behavior accurately 2026-09-29 15:31:15 +08:00
morluto 9aec6908b5 refactor(javascript): remove analysis ceilings 2026-09-29 15:30:43 +08:00
morluto 60cb68a276 refactor(replay): simplify runtime value checks 2026-09-29 14:56:54 +08:00
morluto 271b1620e6 docs: describe complete application analysis outputs 2026-09-29 14:54:01 +08:00
morluto 2a6eb41147 refactor(tools): return complete analysis results inline 2026-09-29 14:53:50 +08:00
morluto c34efca4cd refactor(files): use caller paths for local evidence files 2026-09-29 14:53:20 +08:00
morluto 3f547eafa4 test(browser): cover complete screenshot output 2026-09-29 13:58:55 +08:00
morluto 7eb1a9500c fix(artifacts): stabilize extraction approval 2026-09-29 13:58:47 +08:00
morluto 6740ed5215 refactor(mcp): simplify evidence and workflows 2026-09-29 13:58:26 +08:00
morluto abf7c1142e refactor(process): remove fixed scenario ceilings 2026-09-29 13:57:50 +08:00
morluto 20152a9872 refactor(analysis): remove fixed result quotas 2026-09-29 13:57:24 +08:00
morluto 0e2738ffb6 refactor(browser): return complete observation evidence 2026-09-29 13:56:49 +08:00
morluto 9d1f88cbe8 chore(catalog): sync metadata with final schemas 2026-09-29 13:04:21 +08:00
morluto 6221989f89 chore(catalog): refresh generated tool metadata 2026-09-29 13:03:01 +08:00
morluto f8bb113c22 test(mcp): align workflow assertions with current guidance 2026-09-29 13:02:59 +08:00
morluto f2dc749c86 refactor(process): remove request count ceilings 2026-09-29 13:02:38 +08:00
morluto c523c38027 refactor(observation): return complete inventories 2026-09-29 13:02:30 +08:00
morluto e80fe414b1 refactor(browser): return complete scenario observations 2026-09-29 13:02:20 +08:00
morluto e8c56e0c0a refactor(electron): retain complete active observations 2026-09-29 12:42:24 +08:00
morluto fa2b6c5a61 refactor(observation): retain complete tool results 2026-09-29 12:42:08 +08:00
morluto 34d00fd323 refactor(mcp): simplify workflow and observation scope 2026-09-29 12:41:28 +08:00
morluto 2f604a3c3a docs: regenerate tool and evidence catalogs 2026-09-29 12:17:04 +08:00
morluto a6d5b3ea9d test(artifacts): exercise argument-free extraction preflight 2026-09-29 12:16:53 +08:00
morluto e1d755f7cd refactor(process): remove unused paired experiment 2026-09-29 12:16:49 +08:00
morluto 3fd96506f2 refactor(prompts): make tool workflows optional 2026-09-29 12:16:40 +08:00
morluto 79ea571512 refactor(runtime): retain complete reconciliation outputs 2026-09-29 12:16:32 +08:00
morluto 7b3c797237 refactor(observation): internalize capture budgets 2026-09-29 12:16:10 +08:00
morluto 8c24272ee7 refactor(artifacts): remove selectors and traversal quotas 2026-09-29 12:15:49 +08:00
morluto fe342faa83 test: remove obsolete schema version assertions 2026-09-29 11:42:23 +08:00
morluto 33b20991c4 fix(managed): retain every source location 2026-09-29 11:42:16 +08:00
morluto f2bbd5957e docs: regenerate tool and evidence catalogs 2026-09-29 11:38:32 +08:00
morluto bf9cce1aef docs: correct inline evidence and Hopper behavior 2026-09-29 11:38:24 +08:00
morluto c2a6f4bc43 refactor(ghidra): remove redundant schema markers 2026-09-29 11:38:16 +08:00
morluto 1860d4f0e5 fix(browser): reject unknown Electron input fields 2026-09-29 11:38:09 +08:00
morluto bedd55a4b4 fix(runtime): retain complete reconciled graph content 2026-09-29 11:38:03 +08:00
morluto 24e1fa9a76 refactor(workflows): simplify evidence reference inputs 2026-09-29 11:37:54 +08:00
morluto 53db3a476b docs: refresh tool catalog and cleanup audit fixtures 2026-09-29 11:23:24 +08:00
morluto b3aca5289b refactor(process): remove redundant schema version markers 2026-09-29 11:23:06 +08:00
morluto a8c58c6605 refactor(analysis): remove caller-selected result caps 2026-09-29 11:22:31 +08:00
morluto f55c2b9267 refactor(mcp): simplify tool inputs and inline results 2026-09-29 11:22:00 +08:00
morluto bd0e341572 refactor(tools): remove redundant limits and acknowledgements 2026-09-29 10:53:45 +08:00
morluto 7ba3580c86 refactor(artifacts): select extraction by logical path 2026-09-29 10:53:33 +08:00
morluto dca293a446 refactor(evidence): remove session ledger quotas 2026-09-29 10:53:17 +08:00
morluto 23e56c517c docs: refresh managed conformance manifest 2026-09-29 09:23:28 +08:00
morluto f3645aa51e fix(cli): remove ignored result limits 2026-09-29 09:22:57 +08:00
morluto 7acac615ca refactor(contracts): simplify workflows and drop readiness tool 2026-09-29 09:22:44 +08:00
morluto 0fe96d92df refactor(artifacts): consolidate inventory into inspection 2026-09-29 09:22:24 +08:00
morluto a66e43a99d fix(ghidra): return complete native analysis results 2026-09-29 08:45:49 +08:00
morluto 5457f4a2bc refactor(schema): remove data-only version markers 2026-09-29 08:45:36 +08:00
morluto 9e7605482a refactor(coverage): evaluate inline without workspaces 2026-09-29 08:45:00 +08:00
morluto b3529aa327 test: replace brittle workflow and readme checks 2026-09-29 08:44:41 +08:00
morluto f580d9d073 fix(contracts): align byte read schema with provider 2026-09-29 08:20:22 +08:00
morluto 593a8e12ce fix(runtime): make observed target role optional 2026-09-29 08:20:18 +08:00
morluto 0ae3c0837b test(contracts): remove duplicate tool inventories 2026-09-29 08:19:52 +08:00
morluto a2a3243ea8 refactor: return complete analysis results inline 2026-09-29 08:07:24 +08:00
morluto 07ff8b30ee refactor(artifact): simplify extraction and inventory results 2026-09-29 08:07:04 +08:00
morluto 81996df7a5 refactor: remove cross-version investigation workflow 2026-09-29 08:06:43 +08:00
morluto 0d96275b68 refactor: return complete tool results inline 2026-09-29 07:08:46 +08:00
morlutoandGitHub 2160d4fae5 fix: consolidate boundary ownership and cleanup failure paths (#477) 2026-09-08 10:59:13 +08:00
morluto c490d85045 docs: improve GitHub issue and pull request templates 2026-08-29 10:11:21 +08:00
morluto 07888ec096 fix(ci): retain artifacts for delayed reruns 2026-08-14 14:36:26 +08:00
morlutoandGitHub d4dc940bd8 refactor(test): keep coverage at behavioral boundaries (#474)
* refactor(test): keep coverage at behavioral boundaries

* fix(ci): restore package workflow temp-root classification
2026-08-11 18:34:32 +08:00
morlutoandGitHub d65730c4c2 refactor(domain): encode comparison and lifecycle states exactly (#473)
* refactor(domain): encode comparison states exactly

* refactor(domain): make evidence lifecycles explicit

* refactor(browser): parse capture states once

* test: prefer behavior over implementation seams

* chore: refresh schema-derived metadata

* test(process): preserve lifecycle cleanup coverage

* test(hopper): bind rename cache coverage to dispatch
2026-08-11 18:04:36 +08:00
morlutoGitHubDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
e91c8c39c2 refactor(test): colocate pure-domain tests and reduce test debt (#471)
* refactor(test): colocate pure-domain tests and reduce test debt

Move focused immutable domain/contracts/adapter tests from
tests/composition/** beside their production owners under src/** as
colocated *.test.ts/*.fixture.ts, and relocate persistence-shaped
composition tests under tests/boundary/filesystem/**. Update import
paths, the Vitest project topology (shared worker context for pure
domain/contracts and recording-port service projects), the test
migration ledger, testing docs, and AGENTS.md to match.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* docs: remove test migration ledger

The cutover is complete and the ledger is no longer referenced anywhere
in source, scripts, or the docs gate. Drop the standalone artifact.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* docs: clean up stale migration artifacts

Remove empty directories left by the test migration and replace the
"final cutover" language in docs/testing.md with standing PR guidance
that no longer references the completed migration.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: repair CI failures from schema, knip, and stale generated artifacts

Three root causes fixed:

1. catalog-schemas.mjs: valuesAt only traversed anyOf from Zod's
   toJSONSchema, not oneOf. The webScreenshotDiffSchema change to a
   discriminated union produces oneOf, so the product catalog generator
   could not derive the web_screenshot_diff schema version. Add oneOf
   traversal.

2. conformancePackage.ts: the test rewrite removed the only imports of
   ScenarioManifest, ReplayPlan, ShimPlan, and ExpectedEvidence, making
   them unused exports that knip flags. Remove the dead type aliases.

3. Regenerate stale artifacts (product catalog, MCP tool catalog, skill
   metadata, conformance manifest) that drifted from the source changes.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-10 14:57:44 +08:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
344b9b6dff chore(main): release rea-agents 3.1.0 (#463)
* chore(main): release rea-agents 3.1.0

* chore: normalize release product catalog

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-10 03:47:06 +08:00
morlutoandGitHub bf04d1aea3 fix(release): let release PR updates trigger CI (#470) 2026-08-10 03:46:53 +08:00
morlutoandGitHub 8b62b1a715 docs(npm): simplify current setup command (#469) 2026-08-10 03:24:24 +08:00
morlutoandGitHub 1783bf6dfd fix(npm): preserve explicit setup versions (#465)
* fix(npm): refresh stale setup bootstraps

* fix(npm): preserve explicit setup versions

* docs: distinguish first-run and refresh setup
2026-08-10 02:59:52 +08:00
morlutoandGitHub ed4a62871f docs: clarify the agent reverse-engineering promise (#466)
docs: clarify agent reverse-engineering promise
2026-08-10 02:46:31 +08:00
morlutoandGitHub b8a9da0c0f refactor(mcp): make analysis states unrepresentable (#468)
* refactor(config): parse capability settings into exact states

* refactor(runtime): encode replay and process outcomes exactly

* refactor: make analysis states unrepresentable

* refactor(mcp): register canonical schemas with SDK

* fix: preserve capability and catalog invariants

* fix(build): remove stale compiled modules

* fix(replay): update real verifier policy dependency

* fix(package): verify the complete MCP catalog

* fix(test): supply complete replay reload policy

* refactor(contracts): centralize output schema lookup

* refactor(mcp): delegate protocol boundaries to SDK

* fix(browser): approve real source capture

* fix(mcp): preserve catalog guidance
2026-08-10 02:28:11 +08:00
morlutoandGitHub 6901edc05d fix: harden structured configuration comparisons (#467)
* refactor(process): split ownership observation helpers

* fix: compare structured configuration canonically

* test(conformance): tolerate Markdown line wrapping

* refactor: name caught values explicitly
2026-08-09 11:58:01 +08:00
morlutoandGitHub 8f229ede70 feat(electron): add autonomous Electron runtime analysis (#464)
* feat: add autonomous Electron runtime analysis

* feat(electron): add lifecycle and boundary telemetry

* feat(electron): capture runtime boundary evidence

* fix(electron): package boundary hook helper

* feat(electron): report active capture coverage

* fix(electron): account for bring-your-own runtime

* feat(electron): expand agent-owned runtime scenarios

* fix(ghidra): keep fixture credentials out of argv

* perf(test): serialize local test and docs workloads

* fix(electron): harden active runtime capture

* fix(inspector): require exact runtime target authorization

* fix(docs): make managed manifest host independent

* fix(ci): make generated catalog portable

* fix: unblock autonomous command and electron flows

* fix(electron): close popup and detached helper gaps
2026-08-05 19:07:46 +08:00
morlutoandGitHub 51559245d2 Merge pull request #461 from morluto/agent/comprehensive-repository-audit
fix: harden evidence lifecycle and MCP contracts
2026-08-04 03:00:25 +08:00
morlutoandGitHub 84cc819284 Merge pull request #462 from morluto/agent/fix-release-please-output
fix(ci): guard Release Please PR branch parsing
2026-08-04 01:56:20 +08:00
morluto 6838b6bff9 fix(ci): guard release PR branch parsing 2026-08-04 01:44:28 +08:00
morlutoandGitHub 960b4df056 Merge pull request #455 from morluto/release-please--branches--main--components--rea-agents
chore(main): release rea-agents 3.0.0
2026-08-03 00:23:01 +08:00
github-actions[bot] a83dbb2fb3 chore: normalize release product catalog 2026-08-01 07:18:24 +00:00
github-actions[bot]andGitHub 71f03a7224 chore(main): release rea-agents 3.0.0 2026-08-01 07:17:25 +00:00
morlutoandGitHub 7b6f7becd2 Merge pull request #459 from morluto/agent/verify-npx-upgrade-e2e
test(package): verify npx upgrade on published releases
2026-08-01 15:16:45 +08:00
morluto 1f8ad7bc68 test(package): verify npx upgrade on published releases 2026-08-01 15:10:12 +08:00
morlutoandGitHub 7f46bb1824 Merge pull request #458 from morluto/fix/test-subprocess-coverage
fix(test): retain subprocess coverage and cleanup
2026-07-30 12:54:21 +08:00
morluto dae2b9cc4e fix(test): retain subprocess coverage and cleanup 2026-07-30 04:50:34 +00:00
morlutoandGitHub 48053d1032 Merge pull request #457 from morluto/agent/test-suite-architecture-overhaul
test: overhaul deterministic suite architecture
2026-07-30 12:43:07 +08:00
morluto f0d8cdafa8 fix(test): stabilize boundary timing 2026-07-30 04:37:41 +00:00
morluto 97f6a0f3fd fix(ci): allow documented public type aliases 2026-07-30 04:30:10 +00:00
morluto 92a1c33625 fix(test): honor boundary ownership and public aliases 2026-07-30 04:25:52 +00:00
morluto 8ccdb33dd2 test(contracts): cover validation branches 2026-07-30 04:20:10 +00:00
morluto 788f4057ae fix(ci): make coverage report merging composable 2026-07-30 04:04:45 +00:00
morluto f07acde513 fix(ci): stabilize Vitest project routing and shards 2026-07-30 03:59:44 +00:00
morluto f47ee1abb4 fix(ci): align Vitest project worker budgets 2026-07-29 22:24:54 +00:00
morluto 935721df10 fix(test): bound boundary worker pressure 2026-07-29 22:16:16 +00:00
morluto 3c42403b78 ci: document deterministic suite ownership 2026-07-29 22:02:24 +00:00
morluto a8161f0e55 refactor: preserve provider-neutral runtime seams 2026-07-29 22:02:14 +00:00
morluto 513d9b1555 test: overhaul deterministic suite architecture 2026-07-29 22:00:38 +00:00
morlutoandGitHub 7f2348619a Merge pull request #456 from morluto/agent/mcp-resource-snapshots-context-tools
feat!: make evidence bundles canonical MCP resources
2026-07-30 02:22:51 +08:00
morluto ede91ff1f0 fix: align aggregate tools with session capabilities 2026-07-29 18:18:31 +00:00
morluto f3b3fb07de fix: address MCP resource review feedback 2026-07-29 18:10:45 +00:00
morluto ab4193c1f4 test: derive MCP SDK identity expectations 2026-07-29 17:54:33 +00:00
morluto c824db1ab7 docs: refresh generated SDK metadata 2026-07-29 17:48:20 +00:00
morluto 17d7dbc461 build: upgrade MCP SDK to 2.0.0 2026-07-29 17:42:43 +00:00
morluto 2a45d8de7d docs: document resource snapshot workflow 2026-07-29 17:37:00 +00:00
morluto ba098f24e5 test(eval): cover navigation and address tool routing 2026-07-29 17:36:51 +00:00
morluto 1712e6d821 feat!: make evidence bundles canonical MCP resources
Add immutable session bundle snapshots, bounded resource discovery, paginated unknowns, mutable current analysis snapshots, and provider-neutral navigation/address context tools.

BREAKING CHANGE: export_evidence_bundle now requires a filesystem path and no longer returns an inline bundle.
2026-07-29 17:36:40 +00:00
morlutoandGitHub 3e1e8cd3f8 Merge pull request #454 from morluto/fix/mcp-routing-diagnostics
fix(mcp): clarify target routing and caller diagnostics
2026-07-29 15:32:51 +08:00
morluto 59d465971e docs: refresh managed completion manifest 2026-07-29 07:22:34 +00:00
morluto 281e41f766 fix(verify): follow dynamic Hopper tool availability 2026-07-29 07:18:48 +00:00
morluto 0f96bf365c fix(errors): preserve actionable analysis diagnostics 2026-07-29 07:18:29 +00:00
morluto 969519c00f fix(mcp): clarify routing and advertise input examples 2026-07-29 07:18:09 +00:00
morlutoandGitHub 222ce83967 Merge pull request #444 from morluto/release-please--branches--main--components--rea-agents
chore(main): release rea-agents 2.7.0
2026-07-28 15:02:26 +08:00
github-actions[bot] 7929bf1688 chore: normalize release product catalog 2026-07-28 06:54:15 +00:00
github-actions[bot]andGitHub 8ab40974d3 chore(main): release rea-agents 2.7.0 2026-07-28 06:53:17 +00:00
morlutoandGitHub 91fb87d17e Merge pull request #445 from morluto/codex/conformance-package-336
feat(conformance): add portable conformance export package format
2026-07-28 14:52:18 +08:00
morlutoandGitHub 54153ed62c Merge pull request #446 from morluto/codex/protocol-capture-364
feat(protocol): add gRPC/Protobuf and JSON-RPC/MessagePack capture
2026-07-28 14:52:13 +08:00
morlutoandGitHub f942558573 Merge pull request #447 from morluto/codex/pe-coff-inspection-362
feat(pe): add broader PE/COFF/PDB static inspection
2026-07-28 14:52:08 +08:00
morlutoandGitHub 1fb22b4c21 Merge pull request #448 from morluto/codex/custom-protocol-capture-365
feat(protocol): add custom TCP/UDP/IPC/XPC and auth-flow capture
2026-07-28 14:51:58 +08:00
morlutoandGitHub 1a75f8dc23 Merge pull request #449 from morluto/codex/jvm-python-bytecode-367
feat(bytecode): add JVM and Python bytecode providers
2026-07-28 14:51:17 +08:00
morlutoandGitHub 18624df572 Merge pull request #451 from morluto/codex/mobile-providers-366
feat(mobile): add Android APK/AAB/DEX and iOS IPA static investigation providers
2026-07-28 14:51:11 +08:00
morlutoandGitHub 0ef1b91cf3 Merge pull request #450 from morluto/codex/msix-msi-appx-363
feat(package): add MSIX/MSI/AppX package, resource, and signature analysis
2026-07-28 14:51:07 +08:00
morlutoandGitHub 64f64fec30 Merge branch 'main' into codex/pe-coff-inspection-362 2026-07-28 14:26:38 +08:00
morlutoandGitHub 4602efd27c Merge pull request #452 from morluto/codex/objc-swift-metadata-353
feat(metadata): add deeper ObjC/Swift metadata and database save/licensing
2026-07-28 14:26:25 +08:00
morluto 94e6600faf feat(pe): add broader PE/COFF/PDB static inspection
Expand PE/COFF/PDB inspection to cover imports, exports, resources,
manifests, signatures, debug identifiers, sections, relocations, TLS
callbacks, and architecture metadata (#362).

- peInspection.ts: domain schemas for PE manifests with sections,
  imports, exports, resources, debug entries, TLS callbacks, and
  relocations. Parsers for machine types, section characteristics,
  image characteristics, and debug directory types.
- Public contracts do not expose macOS-only vocabulary.
- Unsupported capabilities are reported explicitly.

Closes #362
2026-07-28 14:22:04 +08:00
morlutoandGitHub 4d228b4d8c Merge branch 'main' into codex/objc-swift-metadata-353 2026-07-28 14:21:35 +08:00
morlutoandGitHub a5e355a3d5 Merge pull request #453 from morluto/codex/event-process-tree-332
feat(process): replace procfs sampling with event-backed process-tree capture
2026-07-28 14:21:25 +08:00
morluto fdeeb27f23 feat(mobile): add Android APK/AAB/DEX and iOS IPA static investigation providers
Add static providers for Android APK/AAB/DEX and Apple IPA artifacts,
including manifests, resources, signing, and native libraries (#366).

- mobileApplicationInvestigation.ts: domain schemas for Android
  (APK, AAB, DEX) and iOS (IPA) artifact formats with manifests,
  signing info, resources, and native libraries.
- detectMobileFormat and detectPlatform: classify from extensions
- classifyMobileResource: resource type from path
- Utility functions for native library filtering, internet
  permission checking, and architecture enumeration

Closes #366
2026-07-28 05:54:34 +00:00
morluto d5d2e56d04 feat(process): replace procfs sampling with event-backed process-tree capture
Replace 50 ms procfs sampling with event-backed capture of root/child/
grandchild spawn, exit, re-exec, and reparent events (#332).

- eventProcessTree.ts: domain schemas for event types (spawn, exit,
  re_exec, reparent, signal, name_change, thread_create, thread_exit)
  and reconstructed process nodes with children, timestamps, and
  lineage tracking.
- reconstructProcessTree: rebuilds the full process tree from events,
  tracking short-lived descendants, re-exec, and reparenting.
- Utility functions: getDescendants, containsProcess, maxTreeDepth.
- Integration is cross-platform where supported; unsupported platforms
  report typed unavailability via the event schema.

Closes #332
2026-07-28 05:47:50 +00:00
morluto d7a07535b0 feat(metadata): add deeper ObjC/Swift metadata and database save/licensing
Add deeper Objective-C and Swift metadata extraction, plus database
path/save/licensing behavior where licensing permits (#353).

- objcSwiftMetadata.ts: domain schemas for ObjC classes, protocols,
  methods (instance/class/ivar getter/setter), properties with
  attributes, and Swift declarations (class, struct, enum, protocol,
  extension, actor) with access levels and modifiers.
- Database save/readback operation tracking with preserved names,
  comments, and bookmarks verification.
- Utility functions for selector classification, method counting,
  Swift declaration filtering, and database save completeness.
- Read-only and mutating operations are distinguished.

Closes #353
2026-07-28 05:46:10 +00:00
morluto 6c03f83b1b feat(package): add MSIX/MSI/AppX package, resource, and signature analysis
Add static analysis for MSIX, MSI, AppX packages, their resources,
and digital signatures (#363).

- packageAnalysis.ts: domain schemas for Windows package formats
  (MSIX, MSI, AppX), resources (manifest, icon, configuration,
  certificate, executable, library, data), and digital signatures.
- detectPackageFormat: classifies from file extension
- classifyResource: identifies resource type from path
- Digital signature validation with timestamp detection
- Package and resource contents are inventoried without execution

Closes #363
2026-07-28 05:42:30 +00:00
morluto 4e0127d620 feat(bytecode): add JVM and Python bytecode providers
Add execution-free providers for JVM bytecode/JAR/WAR and Python
bytecode/wheels/zipapps (#367).

- bytecodeProvider.ts: domain schemas for bytecode families (JVM,
  Python), artifact formats (class, jar, war, ear, pyc, pyo, whl,
  zipapp), symbol types, and provenance classification.
- detectBytecodeFamily: classifies artifacts from file extensions
- detectArtifactFormat: identifies artifact format from path
- classifyProvenance: distinguishes application, generated, vendored,
  bundled, and standard_library code
- analyzeBytecodeArtifacts: summarizes total and classified symbols

Closes #367
2026-07-28 05:42:05 +00:00
morluto 6fa6f9b6d9 feat(protocol): add custom TCP/UDP/IPC/XPC and auth-flow capture
Add support for custom TCP and UDP framing, local IPC, Unix sockets,
named pipes, XPC, and authentication/token lifecycle metadata (#365).

- customProtocolCapture.ts: domain schemas for custom protocol
  frames with transport types (TCP, UDP, IPC, Unix socket, named
  pipe, XPC), frame direction, process identity correlation, and
  decoded content.
- Authentication flow events with stage tracking (init, challenge,
  response, token exchange, renewal, revocation, failure, success).
- Token lifecycle events (issued, refreshed, expired, revoked,
  renewed).
- Credential detection and redaction by default.
- Filters for transport type, protocol, and auth success/failure.

Closes #365
2026-07-28 05:41:41 +00:00
morluto 3f336fefb1 feat(protocol): add gRPC/Protobuf and JSON-RPC/MessagePack capture
Add capture and schema inference for gRPC/Protocol Buffers and
JSON-RPC/MessagePack protocols (#364).

- classifyProtocolFamily: detects protocol from content type or path
- decodeJsonRpc: decodes JSON-RPC messages with credential redaction
- decodeMessagePack: decodes MessagePack wire format (map/array detection)
- decodeGrpcFrame: decodes gRPC 5-byte frame headers
- inferMessageSchema: infers schema hypotheses from repeated messages
- Credential-like fields are detected and redacted by default

Closes #364
2026-07-28 05:40:49 +00:00
morluto 4ee30e4145 feat(conformance): add portable conformance export package format
Define a committed scenario/replay/shim/evidence package format for
portable process-fidelity conformance fixtures (#336). The package
format is versioned, path-independent, and consumable from a clean
checkout.

- conformancePackage.ts: top-level package schema with scenario
  manifests, replay plans, shim plans, expected evidence, and
  verifier contracts. Deterministic package ID computed from
  canonical JSON. Cross-reference validation ensures internal
  consistency.
- conformanceTrustGate.ts: trust gates that compare captured runs
  against committed conformance packages (#337). Classifies semantic
  diffs from timing/normalization noise. Unknown/truncated evidence
  is never treated as equivalence.
- conformanceReplay.ts: CI replay engine that runs each scenario,
  evaluates trust gates, and detects drift between captured and
  committed evidence (#338).

Closes #336, #337, #338
2026-07-28 05:40:17 +00:00
morlutoandGitHub 645668b70d Merge pull request #443 from morluto/codex/provider-catalog-digest
fix(catalog): digest provider projections
2026-07-27 00:35:42 +08:00
morlutoandGitHub ba3151891e Merge branch 'main' into codex/provider-catalog-digest 2026-07-27 00:12:31 +08:00
morluto c4cd0330e0 fix(catalog): digest provider projections 2026-07-26 16:07:12 +00:00
morlutoandGitHub 1caa0d6e54 Merge pull request #438 from morluto/release-please--branches--main--components--rea-agents
chore(main): release rea-agents 2.6.0
2026-07-27 00:00:35 +08:00
github-actions[bot] 17c8b4c2f1 chore: normalize release product catalog 2026-07-26 15:32:38 +00:00
github-actions[bot]andGitHub 58c749e831 chore(main): release rea-agents 2.6.0 2026-07-26 15:31:39 +00:00
morlutoandGitHub 8ec9bb1bfe Merge pull request #442 from morluto/fix/mcp-cold-start
perf(mcp): reduce cold-start work
2026-07-26 23:31:04 +08:00
morluto 7157ab0b70 test(mcp): stabilize lazy-loading verification 2026-07-26 15:26:41 +00:00
morluto fecddc0549 perf(mcp): reduce cold-start work 2026-07-26 15:02:58 +00:00
morlutoandGitHub 6d37efe8fa Merge pull request #440 from morluto/fix/npx-setup-shadowing
fix(setup): refresh stale local npx bootstrap
2026-07-26 15:00:38 +08:00
morluto 90bb1610f5 fix(setup): refresh stale local npx bootstrap 2026-07-26 06:39:28 +00:00
morlutoandGitHub 1a0931b630 Merge pull request #437 from morluto/codex/complete-roadmap-workflows
feat: complete evidence-backed roadmap workflows
2026-07-25 23:30:11 +08:00
morluto 40bf55b74c test(hopper): remove brittle diagnostic source assertion 2026-07-25 15:23:56 +00:00
morlutoandGitHub b2f7561bdd Merge branch 'main' into codex/complete-roadmap-workflows 2026-07-25 23:15:53 +08:00
morluto f2c4edd2bf chore: refresh generated catalog evidence 2026-07-25 15:13:03 +00:00
morluto 4716fa2ece test: add critical provider workflow coverage 2026-07-25 15:12:51 +00:00
morluto 521139d4a5 feat(native): inspect API boundaries with evidence 2026-07-25 14:29:28 +00:00
morluto 5184816199 test(investigation): prove multi-version fixture closure 2026-07-25 14:29:15 +00:00
morluto 9522adf19a test(package): prove artifact cleanup and identity 2026-07-25 14:29:08 +00:00
morluto 7a43420029 feat(hopper): report bridge progress diagnostics 2026-07-25 14:29:02 +00:00
1359 changed files with 86751 additions and 67873 deletions
-39
View File
@@ -1,39 +0,0 @@
---
name: Bug Report
about: Report a bug in REA
title: "[Bug] "
labels: ["bug", "triage"]
assignees: []
---
## Bug Description
A clear and concise description of what the bug is.
## Steps to Reproduce
1.
2.
3.
## Expected Behavior
What you expected to happen.
## Actual Behavior
What actually happened, including any error messages or unexpected output.
## Environment
- REA version (`rea --version` or `npm list @morluto/rea`):
- Node.js version (`node --version`):
- macOS version:
- Hopper Disassembler version:
- Target binary format (Mach-O, ELF, PE, .hop):
## Additional Context
- MCP client and version (if applicable):
- Any relevant logs or stack traces:
- Does the issue reproduce with a different target binary?
+155
View File
@@ -0,0 +1,155 @@
name: Bug report
description: Report a reproducible problem in REA
title: "[Bug] "
labels:
- bug
- triage
body:
- type: markdown
attributes:
value: |
Please report one primary behavior per issue. Keep observed behavior
separate from suspected causes, and remove credentials and private paths.
- type: checkboxes
id: before-submitting
attributes:
label: Before submitting
options:
- label: I searched the open and closed issues for a related report.
required: true
- label: I removed credentials, tokens, private paths, and sensitive target data.
required: true
- type: dropdown
id: area
attributes:
label: Area
options:
- Setup, packaging, or upgrade
- Provider or capability discovery
- Process capture or runtime observation
- Native analysis or decompilation
- Evidence, artifacts, or reconstruction
- CLI
- MCP or agent integration
- Documentation
- Other
validations:
required: true
- type: textarea
id: observed-behavior
attributes:
label: Observed behavior
description: What happened? Include the command, MCP tool, provider, target type, and conditions that matter.
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction
description: Give the smallest realistic command, configuration, input, or event sequence that demonstrates the problem.
placeholder: |
1. Configure or start ...
2. Run ...
3. Observe ...
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: What result, contract, invariant, recovery action, or state transition should have occurred?
validations:
required: true
- type: textarea
id: impact-scope
attributes:
label: Impact and scope
description: What is affected, what is not affected, and does this affect evidence integrity, process isolation, compatibility, or recovery?
validations:
required: true
- type: input
id: rea-version
attributes:
label: REA version
description: Use `rea --version` or provide the source revision.
placeholder: 3.1.0 or git revision
validations:
required: true
- type: input
id: runtime
attributes:
label: Runtime and platform
description: Include Node.js, OS, architecture, and Hopper/Ghidra version when relevant.
placeholder: Node 24.18; macOS 15 arm64; Hopper 5.15
validations:
required: true
- type: input
id: target
attributes:
label: Target and artifact context
description: Include binary format, app/container type, artifact kind, or workload details when relevant.
placeholder: Mach-O arm64 app; Evidence v2 bundle; not applicable
- type: dropdown
id: interface
attributes:
label: Interface
options:
- CLI
- MCP
- Python bridge
- Package or setup
- Other
validations:
required: true
- type: dropdown
id: failure-stage
attributes:
label: Earliest failure stage
description: Select where the incorrect behavior first becomes observable.
options:
- Request parsing or validation
- Setup or provider admission
- Capture or observation
- Artifact or evidence registration
- Native analysis or reconstruction
- Transport or result projection
- Cleanup, cancellation, or lifecycle
- Unknown
validations:
required: true
- type: textarea
id: relevant-evidence
attributes:
label: Relevant output or evidence
description: Paste the smallest useful CLI output, MCP result, error, trace, or evidence excerpt. Redact sensitive values.
render: text
- type: textarea
id: typed-outcome
attributes:
label: Typed outcome or recovery expected
description: Optional. What typed result, error, next tool, cleanup state, or limitation should the caller have received?
- type: textarea
id: analysis
attributes:
label: Analysis or suspected cause
description: Optional. Clearly mark source-derived reasoning or a hypothesis; do not present an unverified cause as fact.
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add links, reduced fixtures, screenshots, or a proposed regression test.
@@ -0,0 +1,69 @@
name: Design discussion
description: Resolve an REA architecture, provider, or contract question before implementation
title: "[Design] "
labels:
- enhancement
- triage
body:
- type: markdown
attributes:
value: |
Use this for a substantial provider, public/persisted contract, or
authority-boundary decision. Keep one decision per discussion.
- type: checkboxes
id: before-submitting
attributes:
label: Before submitting
options:
- label: I searched the open and closed issues and pull requests for a related discussion.
required: true
- label: I removed credentials, tokens, private paths, and sensitive target data.
required: true
- type: textarea
id: decision
attributes:
label: Decision needed
description: State the exact question maintainers need to resolve.
placeholder: Should REA ... or ...?
validations:
required: true
- type: textarea
id: context-evidence
attributes:
label: Context and evidence
description: Explain why the decision is needed now. Separate observed behavior, derived evidence, and inference.
validations:
required: true
- type: textarea
id: boundary-invariants
attributes:
label: Boundary and invariants
description: Identify the owning layer, provider/bridge boundary, artifact or evidence invariants, and any process or authorization constraints.
validations:
required: true
- type: textarea
id: options
attributes:
label: Options and tradeoffs
description: Compare a small number of viable approaches, including platform, compatibility, safety, evidence, and maintenance costs.
validations:
required: true
- type: textarea
id: success-criteria
attributes:
label: Success criteria and open questions
description: State what outcome would settle the decision and list only unresolved questions that could change the choice.
validations:
required: true
- type: textarea
id: references
attributes:
label: References
description: Link relevant contracts, upstream provider documentation, tests, measurements, or prior discussions.
-24
View File
@@ -1,24 +0,0 @@
---
name: Documentation
about: Suggest improvements to REA documentation
title: "[Docs] "
labels: ["documentation", "triage"]
assignees: []
---
## Documentation Area
Which documentation needs improvement? (README, AGENTS.md, API docs, inline JSDoc, skill files)
## Problem
What is unclear, missing, or incorrect in the current documentation?
## Suggested Improvement
Describe the change you would like to see.
## Additional Context
- Link to the relevant documentation file or section:
- Any examples or references that would help:
+53
View File
@@ -0,0 +1,53 @@
name: Documentation
description: Report unclear, missing, or incorrect REA documentation
title: "[Docs] "
labels:
- documentation
- triage
body:
- type: checkboxes
id: before-submitting
attributes:
label: Before submitting
options:
- label: I searched the existing issues for a related documentation report.
required: true
- label: I removed credentials, tokens, private paths, and sensitive target data.
required: true
- type: input
id: location
attributes:
label: Documentation location
description: Link the page, section, command, API, skill, or inline comment.
placeholder: README.md#quick-start or docs/mcp-contracts.md
validations:
required: true
- type: textarea
id: problem
attributes:
label: Problem
description: What is unclear, missing, outdated, or incorrect, and who is affected?
validations:
required: true
- type: textarea
id: expected-content
attributes:
label: Expected content
description: Describe the accurate behavior or example that should be documented.
validations:
required: true
- type: textarea
id: evidence
attributes:
label: Supporting evidence
description: Add source, command output, provider documentation, or a link that supports the correction.
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Include screenshots, affected translations, or related issues.
-29
View File
@@ -1,29 +0,0 @@
---
name: Feature Request
about: Propose a new feature or enhancement for REA
title: "[Feature] "
labels: ["enhancement", "triage"]
assignees: []
---
## Problem Statement
What problem does this feature solve? Describe the use case and why existing tools are insufficient.
## Proposed Solution
Describe the feature or enhancement you would like to see. Include:
- Which REA tool(s) this affects (direct, enhanced, or session tools)
- Expected inputs and outputs
- How it integrates with the existing MCP workflow
## Alternatives Considered
Any alternative approaches or workarounds you have tried.
## Additional Context
- Relevant Hopper API methods or concepts:
- Links to related issues or discussions:
- Would this require changes to the bridge (`bridge/hopper_bridge.py`)?
@@ -0,0 +1,88 @@
name: Feature request
description: Propose a focused improvement to REA
title: "[Feature] "
labels:
- enhancement
- triage
body:
- type: markdown
attributes:
value: |
Keep one primary capability or workflow outcome per request. For a
substantial provider, public contract, or architecture change, explain
the decision maintainers need to make before implementation.
- type: checkboxes
id: before-submitting
attributes:
label: Before submitting
options:
- label: I searched the open and closed issues and pull requests for a related proposal.
required: true
- label: I removed credentials, tokens, private paths, and sensitive target data.
required: true
- type: dropdown
id: area
attributes:
label: Area
options:
- Setup, packaging, or upgrade
- Provider or capability discovery
- Process capture or runtime observation
- Native analysis or decompilation
- Evidence, artifacts, or reconstruction
- CLI
- MCP or agent integration
- Documentation
- Other
validations:
required: true
- type: textarea
id: current-limitation
attributes:
label: Current limitation
description: What user or agent workflow is blocked, ambiguous, or unnecessarily difficult today?
validations:
required: true
- type: textarea
id: use-cases
attributes:
label: Use cases
description: Give concrete applications, targets, callers, or investigation workflows. Explain why current tools are insufficient.
validations:
required: true
- type: textarea
id: desired-outcome
attributes:
label: Desired outcome and success criteria
description: Describe observable behavior, inputs, outputs, evidence, and compatibility criteria that would make this useful.
validations:
required: true
- type: textarea
id: boundaries
attributes:
label: Authority boundaries and non-goals
description: Call out process execution, target mutation, provider licensing, evidence provenance, supported platforms, and work that should remain out of scope.
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: Describe existing REA operations, upstream tools, or workarounds considered and why they do not meet the need.
- type: textarea
id: possible-direction
attributes:
label: Possible direction
description: Optional implementation or integration idea. Treat it as a proposal, not a requirement.
- type: textarea
id: references
attributes:
label: References
description: Link relevant provider documentation, source, measurements, artifacts, issues, or discussions.
+63 -24
View File
@@ -1,34 +1,73 @@
## Description
<!--
PR title: type(optional-scope): imperative outcome
The title becomes the squash-merge commit subject and generated changelog entry.
-->
<!-- What does this change do? Why is it needed? -->
## Summary
## Type of Change
<!-- What user, agent, or maintainer problem does this solve? Link the issue
with "Fixes #123" when applicable. Keep the prior behavior, expected contract,
and new behavior understandable without private context. -->
- [ ] Bug fix
- [ ] New feature
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Refactoring (no functional change)
- [ ] Tool contract change (MCP schema modification)
## Problem and expected behavior
## Testing Performed
<!-- For fixes, give the smallest trigger and violated invariant. For features,
describe the use case and observable outcome. -->
<!-- Describe the testing you have done to verify this change. -->
## Change and scope
- [ ] Unit tests added or updated
- [ ] TypeScript typecheck passes (`npm run typecheck`)
- [ ] Linting passes (`npm run lint`)
- [ ] Full test suite passes (`npm test`)
- [ ] Real Hopper verification performed (`npm run verify:hopper`)
- [ ] Linux real Hopper verification performed (`npm run verify:hopper:linux`) or unavailability explained
- [ ] Real browser verification performed (`npm run verify:browser`) or unavailability explained
- [ ] Package verification performed (`npm run verify:package`)
<!-- Explain the chosen approach, why it fits REA's architecture, and what is
intentionally not included. -->
## Tool Contract Impact
## Contract and boundary impact
<!-- If this PR adds, removes, or modifies MCP tools, list them here. -->
<!-- Canonical inventory and family counts are generated in docs/product-catalog.json. -->
<!-- Complete the applicable lines; use "none" or "not applicable" explicitly. -->
## Related Issues
- Semantic owner and earliest changed stage:
- CLI and MCP/tool-catalog contract:
- Provider, bridge, target-format, or platform compatibility:
- Evidence, artifact, provenance, or reconstruction contract:
- Process execution, authorization, cleanup, or containment impact:
- Generated metadata (`docs/product-catalog.json`), package, or installation impact:
<!-- Link issues this PR closes or relates to. Use "Closes #123" syntax. -->
## Evidence and regression coverage
<!-- State whether evidence is executed, source-derived, or proposed. For fixes,
explain how the regression would fail on the affected base revision when practical. -->
- Tests added or updated:
- Base reproduction or other evidence:
- User-visible CLI/MCP output (if applicable):
- Remaining proof gaps:
For evidence-bearing changes:
- [ ] Observed, derived, and inferred claims remain distinguishable.
- [ ] Artifact identity, source provenance, and failed attempts remain preserved.
- [ ] Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.
## Validation performed
<!-- List only commands that actually ran, with observed results. Include the
relevant real-provider or platform workflow, or explain why it was unavailable. -->
- `command` — result
## Compatibility, safety, and release
<!-- Call out breaking changes, migrations, package effects, platform/provider
limits, security/privacy/containment implications, and meaningful proof gaps. -->
- Breaking changes or migration steps:
- Real Hopper/Ghidra, browser, or OS coverage:
- Package or release metadata impact:
- Security, privacy, process, or containment review:
## Review checklist
- [ ] The PR has one focused outcome and the title follows `type(scope): outcome`.
- [ ] Related issue is linked, or the reason for not linking one is stated above.
- [ ] Tests cover changed observable behavior and meaningful failure paths.
- [ ] Owning docs, contracts, and generated metadata are updated where needed.
- [ ] User-visible CLI/MCP changes include representative output.
- [ ] I checked the final diff for secrets, unrelated cleanup, and unsupported claims.
+97 -20
View File
@@ -88,7 +88,7 @@ jobs:
strategy:
fail-fast: false
matrix:
shard: [1/2, 2/2]
shard: [1/4, 2/4, 3/4, 4/4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@@ -109,7 +109,7 @@ jobs:
include-hidden-files: true
if-no-files-found: error
overwrite: true
retention-days: 1
retention-days: 7
build:
name: Build package
@@ -129,7 +129,7 @@ jobs:
name: rea-dist
path: dist
if-no-files-found: error
retention-days: 1
retention-days: 7
test:
name: Test (coverage + timing)
@@ -160,18 +160,63 @@ jobs:
path: .vitest-reports
pattern: vitest-report-*
merge-multiple: true
- if: needs.changes.outputs.code == 'true' && success()
run: npm run test:ci:merge
- name: Merge coverage, JUnit, and timing reports
if: needs.changes.outputs.code == 'true' && success()
run: >-
npm run test:ci:merge --
--coverage.reportsDirectory=coverage
--coverage.reporter=text
--coverage.reporter=json-summary
--coverage.reporter=lcov
--reporter=default
--reporter=junit
--reporter=json
--outputFile.junit=test-results.xml
--outputFile.json=test-results.json
- name: Summarize slowest test files
if: needs.changes.outputs.code == 'true' && success()
shell: bash
run: |
node <<'NODE' >> "${GITHUB_STEP_SUMMARY}"
const report = JSON.parse(require("node:fs").readFileSync("test-results.json", "utf8"));
const files = (report.testResults ?? [])
.map((result) => ({
name: result.name,
duration: Math.max(0, (result.endTime ?? result.startTime) - result.startTime),
}))
.sort((left, right) => right.duration - left.duration)
.slice(0, 20);
console.log("## Slowest deterministic test files\n");
console.log("| File | Duration (ms) |");
console.log("| --- | ---: |");
for (const file of files) {
console.log(`| \`${file.name}\` | ${file.duration} |`);
}
NODE
- uses: actions/upload-artifact@v4
if: needs.changes.outputs.code == 'true' && always()
with:
name: test-results
path: test-results.xml
path: |
test-results.xml
test-results.json
coverage/
if-no-files-found: error
retention-days: 30
conformance-fixtures:
name: Source-built conformance fixtures
needs: changes
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Verify fixtures from a clean checkout
run: npm run verify:fixtures
package-e2e:
name: Package E2E (${{ matrix.os }})
name: Package/install integration (${{ matrix.os }})
needs: changes
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
strategy:
@@ -188,6 +233,24 @@ jobs:
- run: npm ci
- run: npm run verify:package
apple-artifacts:
name: Real Apple artifact CLI/MCP E2E
needs: changes
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
runs-on: macos-14
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run verify:keyed-archive
- run: npm run verify:asset-catalog
- name: Real binary metadata and Interface Builder integration
run: npm run verify:apple-dispatch && npm run verify:interface-builder
reconstruction-readiness:
name: Reconstruction readiness
needs: changes
@@ -234,7 +297,7 @@ jobs:
run: npm run verify:inspector
windows-curated:
name: Windows Ghidra P0 and package lane
name: Windows capability and package lane
needs: changes
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
runs-on: windows-latest
@@ -255,20 +318,34 @@ jobs:
node -e "const report = JSON.parse(require('node:fs').readFileSync('windows-capabilities.json', 'utf8'));
if (report.platform !== 'win32' || report.architecture !== 'x64')
throw new Error('expected a Windows x64 capability report');"
- name: Run curated Windows P0 tests
# Keep this list aligned with tests/process-global/curatedWindowsLane.test.ts,
# which fails when any path below is renamed or removed. Vitest silently
# ignores unmatched paths, so a stale entry would otherwise report green
# while skipping coverage.
#
# Every path here must pass on windows-latest WITHOUT verified Windows
# native authority. src/ghidra/GhidraProvider.test.ts,
# src/ghidra/GhidraProvider.behavior.test.ts successors, and
# tests/boundary/process/providerProcess.test.ts are deliberately absent:
# they require Job Object process ownership and private-runtime DACL
# enforcement, which this runner does not provide by design. Those claims
# belong behind the same approval boundary as the real Ghidra Windows
# workflow, not in the default lane.
- name: Run curated Windows capability tests
run: >-
npx vitest run --coverage.enabled=false
tests/binaryTarget.test.ts
tests/windowsCapabilities.test.ts
tests/ghidraWindowsFixture.test.ts
tests/ghidraInstallation.test.ts
tests/ghidraProvider.test.ts
tests/ghidraTransport.test.ts
tests/ghidraTargetSnapshot.test.ts
tests/ghidraClient.test.ts
tests/ghidraLauncher.test.ts
tests/processOwnership.test.ts
tests/realGhidraWindowsWorkflow.test.ts
tests/boundary/filesystem/binaryTarget.test.ts
tests/conformance/providers/ghidra/windowsCapabilities.test.ts
tests/conformance/providers/ghidra/ghidraWindowsFixture.test.ts
src/ghidra/GhidraInstallation.test.ts
tests/boundary/providers/ghidra/ghidraTransport.test.ts
tests/boundary/providers/ghidra/ghidraTargetSnapshot.test.ts
tests/boundary/providers/ghidra/ghidraLauncher.test.ts
src/process/ProcessOwnership.test.ts
src/process/ProcessOwnership.part2.test.ts
src/process/ProcessOwnership.part3.test.ts
src/process/ProcessOwnership.validation.test.ts
tests/conformance/providers/ghidra/realGhidraWindowsWorkflow.test.ts
- run: npm run verify:package:windows
- uses: actions/upload-artifact@v4
if: always()
+1 -1
View File
@@ -12,7 +12,7 @@ permissions:
jobs:
verify:
name: Real Ghidra 12.1.2 Windows x64 PE proof
name: Real Ghidra 12.1.4 Windows x64 PE proof
if: github.ref == 'refs/heads/main'
environment: real-ghidra-windows
runs-on: [self-hosted, Windows, x64, ghidra-12-1-2]
+79 -14
View File
@@ -25,7 +25,9 @@ jobs:
- uses: googleapis/release-please-action@v4
id: release
with:
token: ${{ secrets.GITHUB_TOKEN }}
# A dedicated credential lets release-PR updates trigger CI. GitHub
# requires manual approval for pull_request events from GITHUB_TOKEN.
token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
- name: Check out release pull request
if: steps.release.outputs.prs_created == 'true'
@@ -53,9 +55,22 @@ jobs:
if: steps.release.outputs.prs_created == 'true'
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_PR_BRANCH: ${{ fromJSON(steps.release.outputs.pr).headBranchName }}
# Use the same identity for the follow-up commit so it also triggers CI.
GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
RELEASE_PR_JSON: ${{ steps.release.outputs.pr }}
run: |
release_pr_branch="$(node --input-type=module -e '
const value = JSON.parse(process.env.RELEASE_PR_JSON ?? "");
if (
typeof value !== "object" ||
value === null ||
typeof value.headBranchName !== "string" ||
value.headBranchName.length === 0
) {
process.exit(1);
}
process.stdout.write(value.headBranchName);
')"
changed_paths="$(git diff --name-only)"
if [[ -z "${changed_paths}" ]]; then
echo "Release documentation is already canonical."
@@ -72,7 +87,7 @@ jobs:
git add -- docs/product-catalog.json
git commit -m "chore: normalize release product catalog"
gh auth setup-git
git push origin "HEAD:${RELEASE_PR_BRANCH}"
git push origin "HEAD:${release_pr_branch}"
publish:
name: Publish to npm
@@ -97,7 +112,15 @@ jobs:
run: npm run verify:package
- name: Publish
run: npm publish --access public
shell: bash
run: |
version="$(node --input-type=module -e 'import packageJson from "./package.json" with { type: "json" }; process.stdout.write(packageJson.version)')"
published_version="$(npm view "rea-agents@${version}" version --json 2>/dev/null || true)"
if [ "${published_version}" = "\"${version}\"" ]; then
echo "rea-agents@${version} is already published; verifying the existing package."
else
npm publish --access public
fi
- name: Verify published CLI from npm
shell: bash
@@ -106,17 +129,59 @@ jobs:
verification_root="$(mktemp -d)"
trap 'rm -rf -- "${verification_root}"' EXIT
cd "${verification_root}"
for attempt in 1 2 3 4 5 6; do
if npm view "rea-agents@${version}" version --json >/dev/null 2>&1 \
&& npm exec --yes --package="rea-agents@${version}" -- rea --help \
&& npm exec --yes --package="rea-agents@${version}" -- rea --llms \
&& node "${GITHUB_WORKSPACE}/scripts/verify-published-package.mjs" "${version}"; then
exit 0
for attempt in $(seq 1 20); do
published_version="$(npm view "rea-agents@${version}" version --json 2>/dev/null || true)"
if [ "${published_version}" = "\"${version}\"" ]; then
if npm exec --yes --package="rea-agents@${version}" -- rea --help \
&& npm exec --yes --package="rea-agents@${version}" -- rea --llms \
&& node "${GITHUB_WORKSPACE}/scripts/verify-published-package.mjs" "${version}"; then
exit 0
fi
echo "Published rea-agents@${version} is visible, but CLI verification failed on attempt ${attempt}" >&2
else
echo "rea-agents@${version} is not visible on npm yet (attempt ${attempt}/20)" >&2
fi
if [ "${attempt}" -lt 6 ]; then
echo "Published package verification attempt ${attempt} failed; retrying in 10 seconds" >&2
sleep 10
if [ "${attempt}" -lt 20 ]; then
sleep 15
fi
done
echo "Published rea-agents@${version} failed registry and CLI verification" >&2
exit 1
publish-mcp:
name: Publish to MCP Registry
needs: [release-please, publish]
if: needs.release-please.outputs.release_created == 'true' && needs.publish.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.release-please.outputs.tag_name }}
persist-credentials: false
- name: Verify package and registry metadata versions
run: npm run mcp-registry:check
- name: Install MCP Registry publisher
env:
MCP_PUBLISHER_VERSION: v1.8.0
MCP_PUBLISHER_SHA256_LINUX_AMD64: 1370446bbe74d562608e8005a6ccce02d146a661fbd78674e11cc70b9618d6cf
run: |
archive="mcp-publisher_linux_amd64.tar.gz"
curl -fsSLo "${archive}" \
"https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}/${archive}"
echo "${MCP_PUBLISHER_SHA256_LINUX_AMD64} ${archive}" | sha256sum --check --strict
tar xzf "${archive}" mcp-publisher
chmod +x mcp-publisher
- name: Validate MCP Registry metadata
run: ./mcp-publisher validate
- name: Authenticate to MCP Registry
run: ./mcp-publisher login github-oidc
- name: Publish MCP Registry metadata
run: ./mcp-publisher publish
+7 -3
View File
@@ -1,5 +1,6 @@
{
"threshold": 3,
"_comment": "Test files are intentionally NOT excluded: the gate used to report zero clones because it skipped **/*.test.ts, hiding all test-tree duplication. Current actual is ~2.6% lines across src+ and tests/, down from 3.4% when tests were included. Lower this as duplication is removed.",
"threshold": 2.7,
"reporters": ["consoleFull"],
"ignore": [
"node_modules/**",
@@ -7,8 +8,11 @@
"docs/**",
".codex/**",
".devin/**",
"**/*.test.ts",
"tests/fixtures/**"
"tests/fixtures/**",
"tests/conformance/**/*.c",
"tests/conformance/**/*.m",
"tests/conformance/**/*.swift",
"tests/conformance/**/*.xib"
],
"gitignore": true,
"minLines": 5,
+94 -3
View File
@@ -18,10 +18,101 @@
},
"overrides": [
{
"files": ["tests/**/*.ts"],
"files": [
"src/{contracts,domain,server}/**/*.ts",
"src/application/**/*.ts"
],
"excludeFiles": ["src/application/runtime.ts"],
"rules": {
"max-lines-per-function": "off",
"max-lines": "off"
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": ["**/ghidra/**", "**/hopper/**"],
"message": "Deep-provider imports belong behind adapters or in the application composition root."
}
]
}
]
}
},
{
"files": ["tests/**/*.ts", "src/**/*.test.ts", "src/**/*.fixture.ts"],
"rules": {
"max-lines": "off",
"max-lines-per-function": [
"error",
{ "max": 150, "skipBlankLines": true, "skipComments": true }
]
}
},
{
"files": ["src/domain/**/*.{fixture,test}.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": [
"**/application/**",
"**/server/**",
"**/process/**",
"**/browser/**",
"**/ghidra/**",
"**/hopper/**",
"**/replay/**",
"**/cli.*",
"**/main.*"
],
"message": "Domain tests may import only their owning domain and inward dependencies."
}
]
}
]
}
},
{
"files": ["src/application/**/*.test.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": ["**/server/**", "**/cli.*", "**/main.*"],
"message": "Service tests must use application ports rather than CLI or MCP entrypoints."
}
]
}
]
}
},
{
"files": ["tests/composition/**/*.test.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": [
"**/server/**",
"**/cli.*",
"**/main.*",
"node:child_process",
"node:net",
"node:http",
"node:https",
"ws",
"playwright"
],
"message": "Composition tests may wire provider-neutral sessions but must not invoke CLI, MCP, process, network, or browser boundaries."
}
]
}
]
}
}
],
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "2.5.1"
".": "3.2.1"
}
+39 -100
View File
@@ -2,7 +2,7 @@
## Product Direction
REA exposes reverse-engineering tools through a CLI and MCP server. Hopper and the bring-your-own Ghidra adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64 and has an experimental Windows x64 P0 boundary for approved native x86-64 PE applications; it supplies admitted read-only inventory and function-analysis operations but no GUI or mutation authority. Keep provider-specific code out of the domain and application layers.
REA exposes reverse-engineering tools through a CLI and MCP server. Hopper and the bring-your-own Ghidra adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64 and macOS x64/arm64 with matching native decompiler tools, and has an experimental Windows x64 P0 boundary for approved native x86-64 PE applications; it supplies admitted read-only inventory and function-analysis operations but no GUI or mutation authority. Keep provider-specific code out of the domain and application layers.
Prioritize:
@@ -17,121 +17,60 @@ REA is a local-only tool; do not sanitize actionable local diagnostics such as a
## Project Structure & Module Organization
REA is a layered ESM TypeScript application. Dependencies flow inward: `domain` underlies `contracts` and the shared `process` primitives; providers depend on those layers, followed by `application`, `server`, and the entry adapters.
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.
See [docs/architecture.mermaid](docs/architecture.mermaid) for a visual architecture diagram.
- `scripts/rea.mjs`: executable dispatcher. Routes only bare `mcp` and `--mcp` to the production stdio server; Incur handles registration utilities and one-shot commands.
- `src/main.ts`: MCP adapter. Parses config, wires the shared session runtime, starts stdio transport, and owns process-lifetime shutdown.
- `src/cli.ts`: one-shot CLI adapter for setup, diagnostics, analysis, and decompilation.
- `src/config.ts`: Zod-validated parsing of environment configuration into `AppConfig`.
- `src/domain/`: pure, side-effect-free modules. `errors.ts` owns the tagged error algebra; `result.ts` owns `Result`/`ok`/`err`; `hopperValues.ts` owns shared function-dossier values plus Hopper boundary parsers; `symbolAnalysis.ts` parses Swift/ObjC names; `javascriptApplicationGraph.ts` validates and canonically commits the provider-neutral JavaScript Application Graph; `javascriptStaticAnalysis.ts` performs bounded AST-only JavaScript structure recovery.
- `src/contracts/`: caller-visible tool schemas and catalog metadata; `toolContracts.ts` owns the canonical inventory and `enhancedInputs.ts` owns enhanced input parsing.
- `src/process/`: provider-neutral process ownership and lifecycle primitives. It owns private runtime roots, session-assigned run identity and token-verified lineage, absolute startup deadlines, correlated request waits, bounded output capture, and TERM-to-KILL cleanup without defining any provider wire protocol.
- `src/replay/`: Linux x64 controlled-JavaScript-replay adapter. It owns exact runtime closure inspection, Bubblewrap/seccomp/cgroup admission, the disposable worker, strict parent/worker protocol validation, and complete cleanup observation.
- `src/browser/`: loopback CDP/Inspector discovery, bounded WebSocket transport, exact-origin or canonical-root target authorization, passive browser/Electron and attach-only Node/Electron V8 observation, plus controlled Playwright scenarios with explicit launch/attach ownership and cleanup.
- `src/hopper/`: Hopper launch and Unix-socket protocol mechanics. `BridgeLauncher.ts` spawns the Hopper app with the in-process bridge, `HopperClient.ts` correlates request/response over the socket with timeouts and cancellation, `protocol.ts` frames bridge messages.
- `bridge/hopper_bridge.py`: runs inside Hopper and adapts declared operations to Hopper's public Python API. Hopper's bundled MCP server is not used.
- `src/ghidra/`: exact Ghidra 12.1.2/JDK 21 inspection, analysis-profile commitment, digest-bound target snapshots, isolated `analyzeHeadless` launch, authenticated Unix-socket or Windows loopback transport, bounded serial request queue, and strict inventory/function boundaries.
- `src/dotnet/`: execution-free managed PE/CLI inspection. It owns bounded PE, CLI metadata, heap, table, and resource parsing for `rea-dotnet-static`; it must never load, reflect, execute, decompile, or resolve target assemblies.
- `bridge/ghidra/ReaGhidraBridge.java`: packaged read-only `HeadlessScript` loaded through Ghidra's `scriptPath`; it owns the persistent decompiler and adapts admitted inventory, function, reference, and CFG operations to public Ghidra APIs.
- `src/application/`: shared CLI/MCP session composition, setup and diagnostics, and enhanced workflows. `AnalysisProviderRegistry` discovers overlapping deep candidates without starting them; `SessionProviderRouter` binds one candidate per target and composes it with disjoint auxiliary providers; `JavaScriptArtifactReconstruction.ts` safely projects local directories/ASARs and inert bundle syntax into the application graph without adding a caller-visible tool yet.
- `src/server/`: MCP request translation. `createServer.ts` assembles the MCP server, `registerOfficialTools.ts`/`registerEnhancedTools.ts` register each tool set, `toolResult.ts` maps `Result` values to MCP content.
- `docs/product-catalog.json`: generated package, tool-family, provider, setup-client, schema-version, and CLI facts. Regenerate it from source; do not edit it by hand.
- `tests/`: Vitest suite. `tests/fixtures/` holds reusable provider-process fixtures, source-owned synthetic JavaScript graph and Electron/Webpack/Rspack artifact trees, and the fake launcher, Hopper bridge, and CDP seams.
- `scripts/verify-real-hopper.mjs`: real-Hopper end-to-end verifier.
- `scripts/verify-real-ghidra.mjs`: real Linux Ghidra verifier for x86-64 debug/stripped ELF, AArch64 ELF, PE, and Mach-O function semantics plus complete cleanup.
- `scripts/verify-real-ghidra-windows.mjs`: real Windows x64 Ghidra P0 verifier for the source-owned native PE fixture, all 19 admitted operations, digest linkage, loopback transport, and cleanup.
- `scripts/verify-real-browser.mjs`: real Chrome end-to-end verifier for the passive CDP provider.
- `scripts/print-mcp-config.mjs`: prints an MCP server config with absolute paths filled in (`npm run config:print -- /path/to/binary`).
- `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/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.
- `docs/product-catalog.json` is generated. Update its source contracts and regenerate it; do not edit it directly.
## Build, Test, and Development Commands
- `npm ci`: install exact lockfile dependencies.
- `npm run deps:check`: verify that installed direct dependency versions match the root lockfile; on failure, run `npm ci`, then retry.
- `npm run metadata:generate`: refresh checked-in package metadata after package, lockfile, or skill-version changes.
- `npm run metadata:check`: verify checked-in package metadata without rewriting it.
- `npm run build`: compile `src/` into `dist/`.
- `npm run build:cached`: compile through Turbo and reuse a valid local build cache entry.
- `npm test`: restore or build `dist/`, then run the Vitest suite once without coverage or retries locally.
- `npm run test:fast`: run pure and subprocess test projects without the serial integration group.
- `npm run test:changed`: run changed and related non-serial tests once.
- `npm run test:integration`: run the serial filesystem, process, and CLI integration group.
- `npm run test:watch`: watch changed and related non-serial tests, without coverage.
- `npm run test:coverage`: run the complete suite with coverage; CI shards the suite and owns JUnit reporting.
- `npm run typecheck`: run strict TypeScript checks without emitting files.
- `npm run lint`: apply oxlint rules (complexity, max-lines, unused vars, and TypeScript-specific checks).
- `npm run lint:fix`: auto-fix oxlint violations where possible.
- `npm run format:check`: verify Prettier formatting.
- `npm run check`: run cached typecheck, lint, format:check, knip, and package-metadata freshness tasks.
- `npm run check:fast`: run only cached typecheck and lint for rapid local and pre-push feedback.
- `npm run check:ci`: run the static gate plus duplicate-code and debt-marker scans.
- `npm run check:test`: run the cached static tasks plus the uncached complete test suite.
- `npm run check:pr`: run the test gate plus uncached generated-document validation for PR preparation.
- `npm run knip`: detect unused files, dependencies, and exports.
- `npm run jscpd`: detect duplicate code blocks.
- `npm run scan:todos`: scan for TODO, FIXME, and HACK markers.
- `npm run verify:hopper`: build and run the real-Hopper verifier with two distinct binaries.
- `npm run verify:ghidra`: build and run the real-Ghidra verifier against `GHIDRA_INSTALL_DIR` and optional `GHIDRA_TARGET_PATH`.
- `npm run verify:ghidra:windows`: build and run the real Windows Ghidra verifier against `GHIDRA_INSTALL_DIR` and the source-owned x64 PE fixture.
- `npm run verify:browser`: build and run the real Chrome verifier against `REA_BROWSER_EXECUTABLE` or a platform-default Chrome-family executable, including browser-scenario CLI parity, attach disconnect-only ownership, launched-process termination, and temporary-profile cleanup.
- `npm run verify:inspector`: build and run the real attach-only Node Inspector verifier through the public CLI.
- `npm run verify:managed`: build and run the source-owned managed PE/CLI conformance verifier for artifact triage, member inspection, managed/native boundary declarations, token drift comparison, malformed metadata, non-managed degradation, managed application-graph projection, manifest-verifier self-test, and optional BYO ILSpy oracle checks; set `REA_MANAGED_APP_MANIFEST_PATH` to verify an operator-local managed app manifest, including optional graph/trace assertions, and set `REA_ILSPY_CMD_PATH` to an absolute `ilspycmd` path to run the real ILSpy reconstruction oracle.
- `npm run evidence:generate`: regenerate the managed conformance manifest and Evidence v2 completion ledger from live verifier output.
- `npm run evidence:check`: rerun managed conformance and fail on generated manifest, completion ledger, or bundled skill drift.
- `npm run verify:replay`: build and run the real Linux Bubblewrap/seccomp/cgroup verifier against source-owned replay fixtures; set `REA_REPLAY_INPUT_PATH` to verify an operator-local manifest.
- `npm run verify:package`: pack and test the CLI, setup transaction, skill, and canonical target-free MCP catalog in an isolated environment.
- `npm run docs:api`: render uncommitted API documentation from JSDoc comments into `build/api-docs/` using TypeDoc.
- `npm run docs:api:cached`: render API documentation through Turbo or restore a valid local result.
- `npm run docs:generate`: refresh committed metadata and render the uncommitted API documentation.
- `npm run docs:check`: verify generated package metadata, the canonical product catalog, caller-visible documentation facts, TypeDoc rendering, and the error JSON schema.
- `npm run config:print -- /path/to/binary`: print an MCP server config with absolute paths.
- `HOPPER_TARGET_PATH=/path/to/binary npm start`: launch Hopper and run the built stdio MCP server.
- `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 check:fast`: run cached typecheck and lint checks.
- `npm run check:pr`: run the deterministic PR gate and generated-document checks.
- 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.
Pre-commit hooks via Husky format then lint staged source files. Pre-push runs `npm run check:fast`; CI owns the broader static and generated-document gates. Use `npm run lint:fix` to auto-correct violations locally.
See [docs/testing.md](docs/testing.md) for test scopes and verification lanes and [CONTRIBUTING.md](CONTRIBUTING.md) for contribution checks. Pre-commit formats and lints staged source; pre-push runs `npm run check:fast`.
## Configuration & Environment Variables
- `REA_ANALYSIS_PROVIDER` (optional, default `auto`): require one deep-analysis provider ID for startup and one-shot commands, or use deterministic automatic selection. A request-level `provider_id`/`--provider` takes precedence.
- `GHIDRA_INSTALL_DIR` (optional): absolute root of an extracted official Ghidra 12.1.2 distribution. The adapter supports Linux x64 and the experimental Windows x64 P0 boundary documented in `docs/windows-ghidra-p0.md`.
- `JAVA_HOME` (optional): absolute 64-bit full JDK 21 root used by Ghidra. When absent, doctor probes `java`/`javac` from `PATH`.
- `REA_ILSPY_CMD_PATH` (optional): absolute path to a bring-your-own `ilspycmd`; doctor reports its configured version, and `verify:managed` can use it as a real reconstruction oracle without making ILSpy a setup-installed dependency or canonical parser.
- `HOPPER_TARGET_PATH` (optional): absolute initial binary or `.hop` target. Target-free MCP sessions use `open_binary` instead.
- `HOPPER_LAUNCHER_PATH` (optional): override the Hopper executable path (defaults to `/Applications/Hopper Disassembler.app/Contents/MacOS/hopper`).
- `HOPPER_TARGET_KIND` (optional, default `executable`): startup kind for `HOPPER_TARGET_PATH`; dynamic targets are classified from their paths and headers.
- `HOPPER_LOADER_ARGS_JSON` (optional): JSON array overriding derived Hopper loader arguments for supported executable targets, e.g. `["-l","Mach-O","--aarch64"]`.
- `REA_BROWSER_OBSERVE_ENABLED` (optional, default `false`): add browser observation authority to the administrator ceiling.
- `REA_BROWSER_CDP_ENDPOINTS_JSON` (optional): approved literal loopback CDP HTTP endpoints.
- `REA_BROWSER_ALLOWED_ORIGINS_JSON` (optional): exact HTTP(S) page origins approved for passive observation.
- `REA_BROWSER_SCENARIO_ENABLED` (optional, default `false`): add controlled browser automation to the administrator ceiling.
- `REA_BROWSER_SCENARIO_AUTO_GRANT` (optional, default `false`): issue the exact configured browser-automation ceiling as an administrator grant for trusted unattended use.
- `REA_BROWSER_SCENARIO_EXECUTABLE_ROOTS_JSON` (optional): canonical executable roots approved for provider-owned browser launch.
- `REA_BROWSER_SCENARIO_CDP_ENDPOINTS_JSON` (optional): literal-loopback CDP endpoints approved for external-browser attachment.
- `REA_BROWSER_SCENARIO_ALLOWED_ORIGINS_JSON` (optional): exact HTTP(S) page origins approved for browser scenarios.
- `REA_BROWSER_SCENARIO_ALLOWED_ENV_JSON` (optional): environment-variable names that scenario secret references may resolve.
- `REA_ELECTRON_OBSERVE_ENABLED` (optional, default `false`): add passive Electron file-page observation authority to the administrator ceiling.
- `REA_ELECTRON_CDP_ENDPOINTS_JSON` (optional): approved literal loopback Electron CDP HTTP endpoints.
- `REA_ELECTRON_FILE_ROOTS_JSON` (optional): canonical filesystem roots approved for passive Electron page observation.
- `REA_V8_INSPECTOR_OBSERVE_ENABLED` (optional, default `false`): add passive attach-only Node/Electron Inspector observation authority to the administrator ceiling.
- `REA_V8_INSPECTOR_ENDPOINTS_JSON` (optional): approved literal-loopback Node/Electron Inspector HTTP endpoints.
- `REA_V8_INSPECTOR_FILE_ROOTS_JSON` (optional): canonical filesystem roots approved for Inspector targets and script locations.
- `REA_V8_INSPECTOR_ALLOWED_ORIGINS_JSON` (optional): exact HTTP(S) origins approved for Electron renderer Inspector targets and scripts.
- `REA_INVESTIGATION_INPUT_ROOTS_JSON` (optional): canonical filesystem roots approved for static JavaScript/Electron application analysis and other investigation inputs.
- `REA_JAVASCRIPT_REPLAY_ENABLED` (optional, default `false`): add controlled extracted-module execution authority to the administrator ceiling.
- `REA_JAVASCRIPT_REPLAY_ROOTS_JSON` (optional): exact canonical source roots approved for controlled replay.
- `REA_JAVASCRIPT_REPLAY_NODE_PATH`, `REA_JAVASCRIPT_REPLAY_BWRAP_PATH`, `REA_JAVASCRIPT_REPLAY_SYSTEMD_RUN_PATH`, `REA_JAVASCRIPT_REPLAY_SYSTEMCTL_PATH`, and `REA_JAVASCRIPT_REPLAY_SHELL_PATH` (optional): absolute paths committed by each replay plan; defaults target the current Node runtime and standard Linux system executables.
Configuration is parsed and validated by `src/config.ts` and `src/config/`. Keep user-facing setup and provider configuration in [README.md](README.md) and the relevant guide under `docs/`; do not maintain a second environment-variable catalogue here.
## 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.
## 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.
- 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
that benefits from joining evidence sources; keep one-application or
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.
- 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.
- Include artifact identity, source locations, Evidence references, actionable errors, and relevant limitations when they affect conclusions. Return the evidence needed for the next analysis inline rather than requiring a resource or identifier lookup.
- Implement shared application workflows behind CLI and MCP adapters. Update canonical contracts and generated catalog together.
See [docs/tool-design.md](docs/tool-design.md) for the design checklist. When usability or tool selection changes, evaluate representative CLI/MCP tasks as well as schema and transport behavior.
## 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, timeouts, process exit, concurrency, limits, and clean shutdown. 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, 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.
MCP tools are agent-computer interfaces. Optimize them first for successful agent use: distinct purposes, clear names and parameters, relevant result context, actionable errors, and realistic multi-step workflows. Validate tool-design changes with representative agent tasks and transcripts when possible; schema validity and transport success alone do not prove that a tool is easy for an agent to use.
MCP tool catalogs must remain complete and self-describing. Prefer capability- and session-scoped tool advertisement over schema truncation. Never use serialized byte counts as a proxy for model-context cost, token use, tool usability, or agent performance. Measure bytes only when evaluating a named transport, storage, or protocol constraint. Any model-context or token-efficiency claim must identify the client and its actual model-facing projection and tokenizer, since clients may omit, transform, dereference, or otherwise reproject MCP schemas. Do not weaken names, descriptions, schemas, examples, errors, or result context to improve a byte-based metric.
Keep tool catalogs complete and self-describing; prefer capability- and session-scoped availability over schema truncation. Serialized bytes alone do not measure agent usability or model context cost.
## Commit & Pull Request Guidelines
+284
View File
@@ -1,5 +1,289 @@
# Changelog
## [3.2.1](https://github.com/morluto/rea/compare/rea-agents-3.2.0...rea-agents-3.2.1) (2026-10-03)
### Bug Fixes
* **release:** tolerate npm registry propagation delay ([726b730](https://github.com/morluto/rea/commit/726b730b84830b208725df50fa0251ece1fa199f))
### Tests
* **release:** accept guarded npm publishing ([b7bc855](https://github.com/morluto/rea/commit/b7bc8554928c8eca9c29842c77e4021c1e42e054))
## [3.2.0](https://github.com/morluto/rea/compare/rea-agents-3.1.0...rea-agents-3.2.0) (2026-10-03)
### Features
* **native:** add investigation features ([#486](https://github.com/morluto/rea/issues/486)) ([ed933d1](https://github.com/morluto/rea/commit/ed933d18933814e3957a920972ccf91117d4ed87))
### Bug Fixes
* **artifacts:** stabilize extraction approval ([7eb1a95](https://github.com/morluto/rea/commit/7eb1a9500c62b66716878c18ee442c324a715a88))
* **browser:** reject unknown Electron input fields ([1860d4f](https://github.com/morluto/rea/commit/1860d4f0e531919ee9b10bec7ad8db43e18a291b))
* **ci:** retain artifacts for delayed reruns ([07888ec](https://github.com/morluto/rea/commit/07888ec096f69753a1535703fb7b3c3070e786b7))
* **cli:** remove ignored result limits ([f3645aa](https://github.com/morluto/rea/commit/f3645aa51e55c5207f7b9b1f0156ffaef7aaed42))
* consolidate boundary ownership and cleanup failure paths ([#477](https://github.com/morluto/rea/issues/477)) ([2160d4f](https://github.com/morluto/rea/commit/2160d4fae581243990e539a9c401d1b8824e1763))
* **contracts:** align byte read schema with provider ([f580d9d](https://github.com/morluto/rea/commit/f580d9d07382bc9642bce896d431c827955339e4))
* **ghidra:** return complete native analysis results ([a66e43a](https://github.com/morluto/rea/commit/a66e43a99dda1604c8250c9eb73aa8ef000371c9))
* **managed:** retain every source location ([33b2099](https://github.com/morluto/rea/commit/33b20991c455306d66f8687a9f80ea22073d4207))
* **process:** capture complete host probe output ([6c68ff5](https://github.com/morluto/rea/commit/6c68ff565fe1196fd8c7a63598e8bd94098abc3c))
* **runtime:** make observed target role optional ([593a8e1](https://github.com/morluto/rea/commit/593a8e12ceac1cfe350493cfe6b0d1360eb85f7c))
* **runtime:** retain complete reconciled graph content ([bedd55a](https://github.com/morluto/rea/commit/bedd55a4b4ccc9a0c1441d3836138b323de6d126))
### Code Refactoring
* **analysis:** remove caller-selected result caps ([a8c58c6](https://github.com/morluto/rea/commit/a8c58c6605ac37a8b504167732affc5ce4fc772f))
* **analysis:** remove fixed result quotas ([20152a9](https://github.com/morluto/rea/commit/20152a9872dd035714beb5ef5b8898a9b4d1d6c4))
* **artifacts:** consolidate inventory into inspection ([0fe96d9](https://github.com/morluto/rea/commit/0fe96d92dfefbaf28d2abfd5e237f6d0e82eee7b))
* **artifact:** simplify extraction and inventory results ([07ff8b3](https://github.com/morluto/rea/commit/07ff8b30ee497d9f4477a0a9ae97f3f5addbb45a))
* **artifacts:** remove duplicate inventory wrapper ([aa6eee1](https://github.com/morluto/rea/commit/aa6eee1a712d1a319bbaed5387aa5f4429755a1f))
* **artifacts:** remove selectors and traversal quotas ([8c24272](https://github.com/morluto/rea/commit/8c24272ee7e6d30b1493ee7ccd63e45734fe2f95))
* **artifacts:** select extraction by logical path ([7ba3580](https://github.com/morluto/rea/commit/7ba3580c860463adcd5edc73b7a80f34b92480cf))
* **browser:** remove arbitrary capture ceilings ([5dd3b46](https://github.com/morluto/rea/commit/5dd3b4655e21222a6598bf8cbc0be90f3cbf5e0f))
* **browser:** return complete observation evidence ([0e2738f](https://github.com/morluto/rea/commit/0e2738ffb6aa35bb06fe01d0af864aec968570cf))
* **browser:** return complete scenario observations ([e80fe41](https://github.com/morluto/rea/commit/e80fe414b13294446f959b382cb240c806cfe000))
* **browser:** share authorized main-frame polling ([eec75ce](https://github.com/morluto/rea/commit/eec75ce918cbac576fd217847911793331aa32a0))
* **browser:** share canonical identity digest ([a6d9e49](https://github.com/morluto/rea/commit/a6d9e49ab46150c8b757f5bdd5f80d3a8ff52f26))
* **contracts:** remove duplicate schema aliases ([4593c29](https://github.com/morluto/rea/commit/4593c29b64d3f6c43ddb19228da6231f1055eba4))
* **contracts:** simplify workflows and drop readiness tool ([7acac61](https://github.com/morluto/rea/commit/7acac615ca2b8e3f4edd65fd8a08b237f85b5ac7))
* **coverage:** evaluate inline without workspaces ([9e76054](https://github.com/morluto/rea/commit/9e7605482acf2de77ad04c5c5e03ac4280a11d7f))
* **domain:** encode comparison and lifecycle states exactly ([#473](https://github.com/morluto/rea/issues/473)) ([d65730c](https://github.com/morluto/rea/commit/d65730c4c24ab6183426516d4564e6c25294b35d))
* **domain:** share AST property name reader ([4f4cc59](https://github.com/morluto/rea/commit/4f4cc59873ec704811649aeca8a49e3bcdbdf324))
* **electron:** retain complete active observations ([e8c56e0](https://github.com/morluto/rea/commit/e8c56e0c0aeb114e09636ad1841d8fd73720b5cc))
* **errors:** remove stale workspace failures ([f570d40](https://github.com/morluto/rea/commit/f570d40f50bc8621b32ba4e255660ed1b1398356))
* **evidence:** remove obsolete ID resolvers ([a3920e2](https://github.com/morluto/rea/commit/a3920e2abd915eba31b59413710315a85caf1afa))
* **evidence:** remove session ledger quotas ([dca293a](https://github.com/morluto/rea/commit/dca293a446fa86767fb4bed6fe734f09b6cfafde))
* **files:** use caller paths for local evidence files ([c34efca](https://github.com/morluto/rea/commit/c34efca4cd322d4816ab4cd526a7475489edc814))
* **ghidra:** remove redundant schema markers ([c2a6f4b](https://github.com/morluto/rea/commit/c2a6f4bc437671c25ba318d21b82818c8e6bf2ce))
* **javascript:** remove analysis ceilings ([9aec690](https://github.com/morluto/rea/commit/9aec6908b5c7ad50da6882415700cbe53c3f62ed))
* **managed:** derive inspection from PE CLI extents ([dfad09e](https://github.com/morluto/rea/commit/dfad09e3b5769b079bf2d07382aee310d682724f))
* **mcp:** simplify evidence and workflows ([6740ed5](https://github.com/morluto/rea/commit/6740ed5215578eaaf33c785d6245649d76a0411c))
* **mcp:** simplify tool inputs and inline results ([f55c2b9](https://github.com/morluto/rea/commit/f55c2b92676e4a7eb06efbc8d9a908a7de62541a))
* **mcp:** simplify workflow and observation scope ([34d00fd](https://github.com/morluto/rea/commit/34d00fd323c793278c6ed126308c58e2fc7efd54))
* **observation:** internalize capture budgets ([7b3c797](https://github.com/morluto/rea/commit/7b3c7972377c7f77b1328e7d1de3f54835bc1397))
* **observation:** retain complete tool results ([fa2b6c5](https://github.com/morluto/rea/commit/fa2b6c5a61adcb75c4c2773ed1c9f7dba83fc1d3))
* **observation:** return complete inventories ([c523c38](https://github.com/morluto/rea/commit/c523c38027f536e8319eb2acc1840274c5bc5986))
* **permission:** remove unused authorization helpers ([1b0ab75](https://github.com/morluto/rea/commit/1b0ab75a84ccdcf036ccfc019d1890b2d626699a))
* **process:** remove fixed scenario ceilings ([abf7c11](https://github.com/morluto/rea/commit/abf7c1142e759a291d2267fedf15b0dd444a5684))
* **process:** remove redundant schema version markers ([b3aca52](https://github.com/morluto/rea/commit/b3aca5289bf973ffb4072253eebe86e0acced347))
* **process:** remove request count ceilings ([f2dc749](https://github.com/morluto/rea/commit/f2dc749c862a16d056de53e95914a9a411592583))
* **process:** remove unused paired experiment ([e1d755f](https://github.com/morluto/rea/commit/e1d755f7cd574860f59965a6244dbd43f5032f2c))
* **prompts:** make tool workflows optional ([3fd9650](https://github.com/morluto/rea/commit/3fd96506f223889a2d327cc7c97a95bf48ccb594))
* **reference:** share graph index projections ([6ee87fb](https://github.com/morluto/rea/commit/6ee87fbf2defcb10a17352b36ebeed67c6ce6dc5))
* remove cross-version investigation workflow ([81996df](https://github.com/morluto/rea/commit/81996df7a59ae6efb0f7be6687af89e66c64cdcb))
* **replay:** share sandbox probe arguments ([9fca6c0](https://github.com/morluto/rea/commit/9fca6c0296ac360f9d2c838440b24d2ea79234b5))
* **replay:** simplify runtime value checks ([60cb68a](https://github.com/morluto/rea/commit/60cb68a276b012964b08c18e9d60af22e64c468f))
* return complete analysis results inline ([a2a3243](https://github.com/morluto/rea/commit/a2a3243ea874ee1662c62a83ced63be218e0218e))
* return complete tool results inline ([0d96275](https://github.com/morluto/rea/commit/0d96275b6887b72519d2bc54f72012db6a8e482c))
* **runtime:** retain complete reconciliation outputs ([79ea571](https://github.com/morluto/rea/commit/79ea571512a1f55200eb5520d6c56104dba54e5b))
* **schema:** remove data-only version markers ([5457f4a](https://github.com/morluto/rea/commit/5457f4a2bceccb9c40e19a9986964025f279fc91))
* **server:** remove registration alias ([8097ec4](https://github.com/morluto/rea/commit/8097ec456a14992b578057438be8a6e89a076cdf))
* **server:** use canonical elicitation type ([bb6a58c](https://github.com/morluto/rea/commit/bb6a58c2a7eec15fb5f224376230398b51d1b952))
* **test:** colocate pure-domain tests and reduce test debt ([#471](https://github.com/morluto/rea/issues/471)) ([e91c8c3](https://github.com/morluto/rea/commit/e91c8c39c2acddc4389ed0d01950d1954c472e47))
* **test:** keep coverage at behavioral boundaries ([#474](https://github.com/morluto/rea/issues/474)) ([d4dc940](https://github.com/morluto/rea/commit/d4dc940bd871089ffa1b42d3475798b2f4f9c992))
* **tools:** remove arbitrary workflow ceilings ([5f73cb0](https://github.com/morluto/rea/commit/5f73cb061b6fec532d1cccf5a8bce59855823fa4))
* **tools:** remove artificial analysis ceilings ([2f08c7c](https://github.com/morluto/rea/commit/2f08c7c932d3619a481709935f07da59de3d3b27))
* **tools:** remove redundant limits and acknowledgements ([bd0e341](https://github.com/morluto/rea/commit/bd0e34157239613aed7aa9315df0e97d03d8636c))
* **tools:** remove remaining arbitrary workflow caps ([ecf3ed8](https://github.com/morluto/rea/commit/ecf3ed8bdb9ef7a83e7a2fb23443eced6c3824b0))
* **tools:** return complete analysis results inline ([2a6eb41](https://github.com/morluto/rea/commit/2a6eb41147e496b9e044011fa3b755ef74388c51))
* **workflows:** simplify evidence reference inputs ([24e1fa9](https://github.com/morluto/rea/commit/24e1fa9a76e447cff318da9f48c7adb659a1f8ab))
### Documentation
* add REA tool design guidance ([#485](https://github.com/morluto/rea/issues/485)) ([36aeec9](https://github.com/morluto/rea/commit/36aeec95863ca28d3e3aa5bec75a45d3824e17f5))
* clarify aggregate context tools ([c7e207c](https://github.com/morluto/rea/commit/c7e207c290e376db5c2f210f9ac08fe170ef3ec4))
* clarify MCP tool design guidance ([36aeec9](https://github.com/morluto/rea/commit/36aeec95863ca28d3e3aa5bec75a45d3824e17f5))
* correct inline evidence and Hopper behavior ([bf9cce1](https://github.com/morluto/rea/commit/bf9cce1aef89a769320d217e82b895b086880e69))
* describe complete application analysis outputs ([271b162](https://github.com/morluto/rea/commit/271b1620e68f3a6bbc69f28aa4c4d18d5c453baf))
* generalize reverse-engineering workflow ([#488](https://github.com/morluto/rea/issues/488)) ([4ecc7a7](https://github.com/morluto/rea/commit/4ecc7a72f2d6d78b8d9c59a146e532fe552c40bb))
* improve GitHub issue and pull request templates ([c490d85](https://github.com/morluto/rea/commit/c490d85045de07488e01747db9c9f77d264b2e36))
* prioritize reusable analysis primitives ([#493](https://github.com/morluto/rea/issues/493)) ([9dd72c4](https://github.com/morluto/rea/commit/9dd72c4911841671594570677f5ba6e3992d076d))
* refresh managed conformance manifest ([23e56c5](https://github.com/morluto/rea/commit/23e56c517c08abcafe3a81919215433458aef27e))
* refresh tool catalog and cleanup audit fixtures ([53db3a4](https://github.com/morluto/rea/commit/53db3a476b2881f498a5d6c87195e783268c1988))
* regenerate tool and evidence catalogs ([2f604a3](https://github.com/morluto/rea/commit/2f604a3c3ad037a68da4c33a30cb5316983b070f))
* regenerate tool and evidence catalogs ([f2bbd59](https://github.com/morluto/rea/commit/f2bbd5957eccac6164b3f12db87921ffb09bdcf2))
* **tools:** clarify complete inline results ([3b64c81](https://github.com/morluto/rea/commit/3b64c813cf8bcd0cdd262d0a3d5950819f320a12))
* **tools:** clarify complete inline workflows ([4168174](https://github.com/morluto/rea/commit/41681742ccea83a7956e7bc303da4aa223ed4877))
* **tools:** describe uncapped tool behavior accurately ([4cea84d](https://github.com/morluto/rea/commit/4cea84d7cbb0508e6c0dde36a2a0542855681f95))
* **tools:** update inline workflow guidance ([4ad937c](https://github.com/morluto/rea/commit/4ad937c6caeec89ded592ea82dec4fa13ea17d8c))
### Tests
* **artifacts:** exercise argument-free extraction preflight ([a6d5b3e](https://github.com/morluto/rea/commit/a6d5b3ea9d1f53afeda33ef53fe50f42feac2343))
* **browser:** assert strict inputs without legacy fields ([9ddf22e](https://github.com/morluto/rea/commit/9ddf22e1962803bf06d54b2e577a0c03a3f3ad48))
* **browser:** cover complete screenshot output ([3f547ea](https://github.com/morluto/rea/commit/3f547eafa4c6f27b8264025c754392161687908e))
* **browser:** remove stale input field assertion ([60c110d](https://github.com/morluto/rea/commit/60c110d97b1b22c010b8597965b3ad09f4f51480))
* **cli:** consolidate duplicate setup journey ([9bd7e2c](https://github.com/morluto/rea/commit/9bd7e2cb459d0354ee2c406555de9e427c549086))
* **cli:** remove duplicate browser discovery case ([dcb0362](https://github.com/morluto/rea/commit/dcb036292e975c81780dbf7a711fd1a565a13a10))
* **conformance:** remove fixture generator test option ([ea853ce](https://github.com/morluto/rea/commit/ea853cebde5641dc9c12ef59005de84197463467))
* **contracts:** remove duplicate tool inventories ([0ae3c08](https://github.com/morluto/rea/commit/0ae3c0837b387e53686bcd6e8aa03cb29f80458a))
* **hopper:** name regex search assertion accurately ([9240bd8](https://github.com/morluto/rea/commit/9240bd8d166ffee93a90fd55c5de4b58d8ff6ad4))
* **hopper:** remove dossier identity assertion ([b5c0b5b](https://github.com/morluto/rea/commit/b5c0b5b53e2d15dd6da9d4e8b1d3c3c21a16c409))
* **mcp:** align workflow assertions with current guidance ([f8bb113](https://github.com/morluto/rea/commit/f8bb113c22cf23916a8cd8a5d77734ccaf63fd08))
* **mcp:** consolidate catalog inventory checks ([3afcd64](https://github.com/morluto/rea/commit/3afcd641462bbd99ebdc3bf2fc8264e2c3344c09))
* **mcp:** validate defaults at tool boundary ([21241d2](https://github.com/morluto/rea/commit/21241d2323d323f405b3cff4692e01364b6ddbde))
* remove duplicate export schema acceptance ([b783e7f](https://github.com/morluto/rea/commit/b783e7febef8c22b41817a61a7f8293393ac6425))
* remove obsolete schema version assertions ([fe342fa](https://github.com/morluto/rea/commit/fe342faa832ec9b17a6fb1dd85b1cf269b33cf1b))
* remove stale limits and handoff expectations ([acf33d6](https://github.com/morluto/rea/commit/acf33d6df272873728aaa7527c5d43fa2a13132b))
* remove vacuous and duplicate assertions ([56e2bf7](https://github.com/morluto/rea/commit/56e2bf7696e6ccbc3e4b5baf9249c6d4152f4a43))
* remove Vitest config test exports ([d6e545c](https://github.com/morluto/rea/commit/d6e545cc4832ed47cc98d974aa8e4aa9453afb98))
* replace brittle workflow and readme checks ([b3529aa](https://github.com/morluto/rea/commit/b3529aa327026472798a5f0971dcb325d221ddc6))
* replace implementation lock-in with behavioural assertions ([#498](https://github.com/morluto/rea/issues/498)) ([7ee3f23](https://github.com/morluto/rea/commit/7ee3f236a9f753dfc90d84f95f20598e4e45fd0d))
* **replay:** remove duplicate schema coverage ([4ec2343](https://github.com/morluto/rea/commit/4ec2343d91925e583b15d5404af2eabaf6ffed0f))
## [3.1.0](https://github.com/morluto/rea/compare/rea-agents-3.0.0...rea-agents-3.1.0) (2026-08-09)
### Features
* **electron:** add autonomous Electron runtime analysis ([#464](https://github.com/morluto/rea/issues/464)) ([8f229ed](https://github.com/morluto/rea/commit/8f229ede70c0a4e224c1cf71d61487e17b779d6a))
### Bug Fixes
* **ci:** guard Release Please PR branch parsing ([84cc819](https://github.com/morluto/rea/commit/84cc819284b9ce7ee648850c85d5daf3a33e5a24))
* **ci:** guard release PR branch parsing ([6838b6b](https://github.com/morluto/rea/commit/6838b6bff9ab5c400432caefd8c8737b86c41f07))
* harden evidence lifecycle and MCP contracts ([5155924](https://github.com/morluto/rea/commit/51559245d2e1c2d7550cfe489716950fe6154bd0))
* harden structured configuration comparisons ([#467](https://github.com/morluto/rea/issues/467)) ([6901edc](https://github.com/morluto/rea/commit/6901edc05d56063bdbe8be96511b1b21863630b5))
* **npm:** preserve explicit setup versions ([#465](https://github.com/morluto/rea/issues/465)) ([1783bf6](https://github.com/morluto/rea/commit/1783bf6dfdd4ec11e824870ce5b1a2f5a321450e))
### Code Refactoring
* **mcp:** make analysis states unrepresentable ([#468](https://github.com/morluto/rea/issues/468)) ([b8a9da0](https://github.com/morluto/rea/commit/b8a9da0c0f9f0b2b022a350d2c7c671d1fa3f886))
### Documentation
* clarify agent reverse-engineering promise ([ed4a628](https://github.com/morluto/rea/commit/ed4a62871f7924948e3774ae6cf880514a560ff4))
* clarify the agent reverse-engineering promise ([#466](https://github.com/morluto/rea/issues/466)) ([ed4a628](https://github.com/morluto/rea/commit/ed4a62871f7924948e3774ae6cf880514a560ff4))
* **npm:** simplify current setup command ([#469](https://github.com/morluto/rea/issues/469)) ([8b62b1a](https://github.com/morluto/rea/commit/8b62b1a71586e9409e4dd645f3d4947ef61e59b1))
## [3.0.0](https://github.com/morluto/rea/compare/rea-agents-2.7.0...rea-agents-3.0.0) (2026-08-01)
### ⚠ BREAKING CHANGES
* export_evidence_bundle now requires a filesystem path and no longer returns an inline bundle.
### Features
* make evidence bundles canonical MCP resources ([1712e6d](https://github.com/morluto/rea/commit/1712e6d821965ce5edc3880fe4ebab2e3cc02fd4))
### Bug Fixes
* address MCP resource review feedback ([f3b3fb0](https://github.com/morluto/rea/commit/f3b3fb07ded3d1ad72c0a4f24ca3147ef9722c5d))
* align aggregate tools with session capabilities ([ede91ff](https://github.com/morluto/rea/commit/ede91ff1f04563001f695ec3869daeb3c826154a))
* **ci:** align Vitest project worker budgets ([f47ee1a](https://github.com/morluto/rea/commit/f47ee1abb4fc8dbd5da3fa13c5870d6580b28bfa))
* **ci:** allow documented public type aliases ([97f6a0f](https://github.com/morluto/rea/commit/97f6a0f3fd2ebf8fa884d7f7ba6e1c66ecfe80e0))
* **ci:** make coverage report merging composable ([788f405](https://github.com/morluto/rea/commit/788f4057aee65dfae441249d2c9789bb996912a7))
* **ci:** stabilize Vitest project routing and shards ([f07acde](https://github.com/morluto/rea/commit/f07acde513d4c74d027b66e143cf5072246b412b))
* **errors:** preserve actionable analysis diagnostics ([0f96bf3](https://github.com/morluto/rea/commit/0f96bf365c330a7eb121a173bfa140cc2b652764))
* **mcp:** clarify routing and advertise input examples ([969519c](https://github.com/morluto/rea/commit/969519c00f31b5bc0f23dcffe8867e091461fda6))
* **mcp:** clarify target routing and caller diagnostics ([3e1e8cd](https://github.com/morluto/rea/commit/3e1e8cd3f829e6d3ac8b3c63d979f9db84dec826))
* **test:** bound boundary worker pressure ([935721d](https://github.com/morluto/rea/commit/935721df10d25d23621dbc2be90d334644e0858c))
* **test:** honor boundary ownership and public aliases ([92a1c33](https://github.com/morluto/rea/commit/92a1c33625ff88d81581554687e7708dd3c91e2d))
* **test:** retain subprocess coverage and cleanup ([7f46bb1](https://github.com/morluto/rea/commit/7f46bb18241bf3815e5e01816277d0cafe8f923b))
* **test:** retain subprocess coverage and cleanup ([dae2b9c](https://github.com/morluto/rea/commit/dae2b9cc4e8753827702ada6e51f2ee01811cfed))
* **test:** stabilize boundary timing ([f0d8cda](https://github.com/morluto/rea/commit/f0d8cdafa86bafdc6ad5d8c2d23a97ef583b53d5))
* **verify:** follow dynamic Hopper tool availability ([281e41f](https://github.com/morluto/rea/commit/281e41f7660066addcc7a204128fb1f49bf7a674))
### Code Refactoring
* preserve provider-neutral runtime seams ([a8161f0](https://github.com/morluto/rea/commit/a8161f0e5540f3261a08015dfe56a4254980c064))
### Documentation
* document resource snapshot workflow ([2a45d8d](https://github.com/morluto/rea/commit/2a45d8de7d52d0a234672d03c7dae794859f8ced))
* refresh generated SDK metadata ([c824db1](https://github.com/morluto/rea/commit/c824db1ab7aed2886e9050c1b7fe2136b77e3698))
* refresh managed completion manifest ([59d4659](https://github.com/morluto/rea/commit/59d465971ea86f933e9037fa32ac2787d935d076))
### Tests
* **contracts:** cover validation branches ([8ccdb33](https://github.com/morluto/rea/commit/8ccdb33dd25922e039140bcd1e3cbf3d5acee837))
* derive MCP SDK identity expectations ([ab4193c](https://github.com/morluto/rea/commit/ab4193c1f48020ff6baa42cb75e033110ee72b36))
* **eval:** cover navigation and address tool routing ([ba098f2](https://github.com/morluto/rea/commit/ba098f24e59efbd567ae8438f65e31af0f0b8050))
* overhaul deterministic suite architecture ([48053d1](https://github.com/morluto/rea/commit/48053d1032324cce6e2fe782ecba43d1b91a1bba))
* overhaul deterministic suite architecture ([513d9b1](https://github.com/morluto/rea/commit/513d9b15558f9174a8e2c0ab00e31cbc05dbaba7))
* **package:** verify npx upgrade on published releases ([7b6f7be](https://github.com/morluto/rea/commit/7b6f7becd257170cebf80f4f9f9c385b0000f074))
* **package:** verify npx upgrade on published releases ([1f8ad7b](https://github.com/morluto/rea/commit/1f8ad7bc68f50d396715116467f19524da7e05e6))
### Continuous Integration
* document deterministic suite ownership ([3c42403](https://github.com/morluto/rea/commit/3c42403b78b53e7b5358c2bca9bc156ef2c32efd))
## [2.7.0](https://github.com/morluto/rea/compare/rea-agents-2.6.0...rea-agents-2.7.0) (2026-07-28)
### Features
* **bytecode:** add JVM and Python bytecode providers ([1a75f8d](https://github.com/morluto/rea/commit/1a75f8dc236d8269a5fa07e042471ac79e79d728))
* **bytecode:** add JVM and Python bytecode providers ([4e0127d](https://github.com/morluto/rea/commit/4e0127d620377e5dc7e09444f9db0977e3d47ed1)), closes [#367](https://github.com/morluto/rea/issues/367)
* **conformance:** add portable conformance export package format ([91fb87d](https://github.com/morluto/rea/commit/91fb87d17e7885439762667c96167af79a2e044e))
* **conformance:** add portable conformance export package format ([4ee30e4](https://github.com/morluto/rea/commit/4ee30e4145db9b496158cd980af05e22010856a7))
* **metadata:** add deeper ObjC/Swift metadata and database save/licensing ([4602efd](https://github.com/morluto/rea/commit/4602efd27cbf8701c2dc15542042bc9d9ef76e7f))
* **metadata:** add deeper ObjC/Swift metadata and database save/licensing ([d7a0753](https://github.com/morluto/rea/commit/d7a07535b0fb61ee8f9ae648bb673dda23290653)), closes [#353](https://github.com/morluto/rea/issues/353)
* **mobile:** add Android APK/AAB/DEX and iOS IPA static investigation providers ([18624df](https://github.com/morluto/rea/commit/18624df5724a2c4d14a4e8e178660456424b2344))
* **mobile:** add Android APK/AAB/DEX and iOS IPA static investigation providers ([fdeeb27](https://github.com/morluto/rea/commit/fdeeb27f23041c19b5857fe7873106f0cd9c6244)), closes [#366](https://github.com/morluto/rea/issues/366)
* **package:** add MSIX/MSI/AppX package, resource, and signature analysis ([0ef1b91](https://github.com/morluto/rea/commit/0ef1b91cf31f6ca53991ee5ca60b4a22f430c01e))
* **package:** add MSIX/MSI/AppX package, resource, and signature analysis ([6c03f83](https://github.com/morluto/rea/commit/6c03f83b1bd55095d136aeb3700733383365ecc4)), closes [#363](https://github.com/morluto/rea/issues/363)
* **pe:** add broader PE/COFF/PDB static inspection ([f942558](https://github.com/morluto/rea/commit/f9425585736a89165390178f245b0f0f5d6cdacf))
* **pe:** add broader PE/COFF/PDB static inspection ([94e6600](https://github.com/morluto/rea/commit/94e6600faf89436e4e2689fda8050792307b268d)), closes [#362](https://github.com/morluto/rea/issues/362)
* **process:** replace procfs sampling with event-backed process-tree capture ([a5e355a](https://github.com/morluto/rea/commit/a5e355a3d52b19e89535449cd923c35e56ccc60d))
* **process:** replace procfs sampling with event-backed process-tree capture ([d5d2e56](https://github.com/morluto/rea/commit/d5d2e56d0461cb0debba540bc658a9938ac30599)), closes [#332](https://github.com/morluto/rea/issues/332)
* **protocol:** add custom TCP/UDP/IPC/XPC and auth-flow capture ([1fb22b4](https://github.com/morluto/rea/commit/1fb22b4c21e3a5581f3928003cc32ddc1cc9530b))
* **protocol:** add custom TCP/UDP/IPC/XPC and auth-flow capture ([6fa6f9b](https://github.com/morluto/rea/commit/6fa6f9b6d9595a480a590766dadcfebb8e7111b5)), closes [#365](https://github.com/morluto/rea/issues/365)
* **protocol:** add gRPC/Protobuf and JSON-RPC/MessagePack capture ([54153ed](https://github.com/morluto/rea/commit/54153ed62cb4f55d1efd4794490c2481ab34e212))
* **protocol:** add gRPC/Protobuf and JSON-RPC/MessagePack capture ([3f336fe](https://github.com/morluto/rea/commit/3f336fefb132971bc6bb37b2b59aa01a5e3be5ca)), closes [#364](https://github.com/morluto/rea/issues/364)
### Bug Fixes
* **catalog:** digest provider projections ([645668b](https://github.com/morluto/rea/commit/645668b70dfdff74582d4ac26cda417947231764))
* **catalog:** digest provider projections ([c4cd033](https://github.com/morluto/rea/commit/c4cd0330e00eeb8e272abbf6ecc6ce961e72f317))
## [2.6.0](https://github.com/morluto/rea/compare/rea-agents-2.5.1...rea-agents-2.6.0) (2026-07-26)
### Features
* complete evidence-backed roadmap workflows ([1a0931b](https://github.com/morluto/rea/commit/1a0931b630587701d495d1634fbafe3638ad5516))
* **hopper:** report bridge progress diagnostics ([7a43420](https://github.com/morluto/rea/commit/7a434200297df85c62c02bb103cf870d14f202f7))
* **native:** inspect API boundaries with evidence ([521139d](https://github.com/morluto/rea/commit/521139d4a57a8ab2e25bb262d2a1fd09fd06be92))
### Bug Fixes
* **setup:** refresh stale local npx bootstrap ([6d37efe](https://github.com/morluto/rea/commit/6d37efe8fa1fb9b09c8eaa3a1ed6b6f63188ae22))
* **setup:** refresh stale local npx bootstrap ([90bb161](https://github.com/morluto/rea/commit/90bb1610f56448846adf85b31d9ae96be12dfb4a))
### Performance Improvements
* **mcp:** reduce cold-start work ([8ec9bb1](https://github.com/morluto/rea/commit/8ec9bb1bfea8bc57441bec72f3fcfbe93cf5d4f4))
* **mcp:** reduce cold-start work ([fecddc0](https://github.com/morluto/rea/commit/fecddc0549a31e99a5ea72cbda84e448b1a961d5))
### Tests
* add critical provider workflow coverage ([4716fa2](https://github.com/morluto/rea/commit/4716fa2ece954f5e53bdda9d1f33afad57d2e151))
* **hopper:** remove brittle diagnostic source assertion ([40bf55b](https://github.com/morluto/rea/commit/40bf55b74ca6b2c90b3243a29018cb75879ba081))
* **investigation:** prove multi-version fixture closure ([5184816](https://github.com/morluto/rea/commit/5184816199895ceff6a489810e12fba0c2c41eb7))
* **mcp:** stabilize lazy-loading verification ([7157ab0](https://github.com/morluto/rea/commit/7157ab0b70833cc8237271e8625ccb7965c318c3))
* **package:** prove artifact cleanup and identity ([9522adf](https://github.com/morluto/rea/commit/9522adf19ac915247e2c74b18d3ef5cab95befc5))
## [2.5.1](https://github.com/morluto/rea/compare/rea-agents-2.5.0...rea-agents-2.5.1) (2026-07-25)
+9 -9
View File
@@ -2,6 +2,8 @@
REA welcomes focused bug fixes, documentation improvements, tests, and reverse-engineering workflow enhancements. Open an issue before a large contract or architecture change so its scope can be agreed before implementation.
When adding or changing an MCP tool, follow the [tool design guide](docs/tool-design.md) and preserve the canonical contracts and generated catalog.
## Development setup
REA development requires Node.js 24.18.x and npm 11.16.x. Real-Hopper verification additionally requires either macOS 12+ or an officially supported Linux host (Ubuntu 24.04+, Fedora 41+, or 64-bit Arch) and an installed Hopper application. Linux demo verification uses its own private Xvfb display and does not require a desktop session. Run `nvm use` before installing dependencies.
@@ -38,15 +40,13 @@ 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.
Local `npm test` runs without coverage, retries, or verbose output. Use `npm run
test:fast` for pure and subprocess groups, `npm run test:integration` for serial
filesystem/process/CLI cases. `npm run test:changed` runs changed and related
non-serial tests once; `npm run test:watch` keeps that selection live. Pure tests
use Vitest threads, while subprocess tests stay in isolated forks. CI splits the
complete suite across two native Vitest
shards, then merges their coverage and JUnit reports. `npm run test:ci` runs the
equivalent unsharded gate locally. Coverage thresholds remain in
`vitest.config.ts`.
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`.
CI installs dependencies once for all static checks, cancels superseded PR
runs, and skips package, Windows, and full test lanes for documentation-only
+91 -104
View File
@@ -4,7 +4,7 @@
# REA: Reverse Engineer Anything
### One CLI and MCP server for agents to reverse engineer anything
### Reverse engineer anything with agents, from app behavior down to native binaries.
**See a feature you like. Understand how it works, down to the binary level.**
@@ -30,7 +30,7 @@
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.
REA gives agents one consistent way to investigate software. Today that includes deep native analysis and function dossiers through Hopper or bring-your-own Ghidra on Linux, plus an experimental Windows x64 Ghidra P0 for approved native PE applications; execution-free managed PE/CLI triage; reproducible Evidence v2 records; controlled process capture; passive website, Electron page, and Node/Electron V8 Inspector observation; bounded JavaScript/source-map reconstruction; and a versioned domain graph for connecting JavaScript 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 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.
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.
@@ -89,13 +89,13 @@ REA shows how it reached its conclusions. It does not claim to recover original
### Run setup — recommended
```bash
npx rea-agents setup
npx --yes rea-agents@latest 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. `npx rea-agents setup` opens with the work it
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
@@ -103,6 +103,11 @@ detected agents, then asks which capabilities to set up: agent integration
Nothing is preselected. Choosing agent integration opens a second empty
checklist for the specific detected agents that should receive a registration.
`@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.
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
@@ -132,7 +137,7 @@ Pass installer options after `bash -s --`, for example `--dry-run`, `--no-setup`
### With an agent — recommended
```bash
npx rea-agents setup
npx --yes rea-agents@latest setup
```
Choose Agent Integration in the reviewed setup plan. REA installs the pinned MCP
@@ -144,7 +149,7 @@ Review the setup plan, approve it if appropriate, then describe the app or featu
### From Terminal — no installation
```bash
npx rea-agents setup
npx --yes rea-agents@latest setup
npx -y rea-agents@latest doctor
npx -y rea-agents@latest analyze /Applications/Notes.app
```
@@ -221,10 +226,10 @@ REA defaults `HOPPER_LAUNCHER_PATH` to `/Applications/Hopper Disassembler.app/Co
### Ghidra read-only analysis provider
The Ghidra adapter supports the exact official Ghidra 12.1.2 release with a 64-bit full JDK 21 on Linux x64. It 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:
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:
```bash
export GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra_12.1.2_PUBLIC
export GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra_12.1.4_PUBLIC
export JAVA_HOME=/absolute/path/to/jdk-21 # optional when java and javac resolve from PATH
rea doctor --json
rea setup
@@ -235,15 +240,17 @@ Doctor distinguishes missing configuration, a bad installation root, the wrong G
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.
REA loads its packaged Java `HeadlessScript` with `-scriptPath`, copies and digest-verifies the target in an ephemeral runtime, enables `-readOnly` and `-deleteProject`, caps auto-analysis at 300 seconds with two CPUs and a 2 GiB Java heap, and authenticates every request. Linux uses a mode-0600 Unix socket. Windows P0 uses token-authenticated IPv4 loopback and a token-free endpoint record because Node path-based IPC does not connect to Java AF_UNIX sockets on Windows. The bridge verifies Ghidra's imported-byte SHA-256 before serving any operation.
REA 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.
The Ghidra adapter declares 19 direct and enhanced operations. Its ten inventory operations are `list_documents`, `list_procedures`, `list_strings`, `list_names`, `list_segments`, `address_name`, `procedure_address`, `resolve_containing_procedure`, `search_procedures`, and `search_strings`. It also admits `procedure_info`, `procedure_pseudo_code`, `procedure_assembly`, `read_function_instructions`, `procedure_callers`, `procedure_callees`, `procedure_references`, `xrefs`, and `analyze_function`. `read_function_instructions` is the offset-paginated fast path for raw instruction windows: it does not invoke the decompiler or whole-program name/string inventories, and is also exposed as `rea instructions`. These capabilities enable the shared Swift/Objective-C inventory workflows, `binary_overview`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, `trace_feature`, and complete function dossiers. Default-space addresses are lowercase `0x` hexadecimal. Other spaces, including `EXTERNAL`, use `<percent-encoded-space>:0x<hex>`. Symbol results identify primary, dynamic, external, type, and source facts; procedures distinguish external functions and thunks; strings identify charset, missing-terminator state, byte length, and value truncation; memory-block ends are exclusive and permissions come directly from Ghidra.
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.
The bridge serves operations only after auto-analysis completes. Each Program owns one persistent `DecompInterface`; a bounded 32-request FIFO serializes Ghidra API access, and every decompile has a 30-second native 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. An analysis timeout, scan or inventory safety limit, request timeout, or oversized response fails explicitly instead of returning a partial result labeled complete.
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` compiles source-owned x86-64 debug and stripped ELF, AArch64 ELF, x86-64 PE, and x86-64 Mach-O fixtures. Against real Ghidra 12.1.2 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. Set `REA_CC`, `REA_CLANG`, or `REA_LLD_LINK` only when the corresponding compiler command is not on `PATH`.
`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:windows` uses a deterministic source-owned native x86-64 PE application and requires all 19 operations, target/snapshot/import digest linkage, authenticated loopback transport, and cleanup on a controlled Windows x64 Ghidra 12.1.2 runner. This proof does not establish Job Object ownership, private DACLs, or reparse-point-safe authority.
`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.
To remove only REA-owned MCP registrations and the managed skill:
@@ -252,7 +259,7 @@ rea uninstall
rea uninstall --purge-data # also removes only ~/.rea/cache and ~/.rea/state
```
Uninstall preserves Hopper, Node.js, evidence, captures, external evidence roots, unrelated skills, and other MCP servers. It refuses malformed client configuration and never follows purge-data symlinks.
Uninstall preserves Hopper, Node.js, Evidence files, captures, unrelated skills, and other MCP servers. It refuses malformed client configuration and never follows purge-data symlinks.
### CLI or agent?
@@ -260,73 +267,54 @@ Uninstall preserves Hopper, Node.js, evidence, captures, external evidence roots
| ---------------------------------------------------------------- | ------------------------------------------------------------------------- |
| 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 v2 bundles | `rea evidence-import`, `rea evidence-export`, or `rea compare` |
| Run or resume a persistent two-version artifact analysis | `rea investigate-versions` |
| Map a local JavaScript/Electron application without executing it | `rea analyze PATH --approved` or `rea analyze-javascript-application` |
| 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` |
Filesystem evidence commands and MCP file tools are disabled until the operator approves absolute roots:
```bash
export REA_EVIDENCE_ROOTS_JSON='["/absolute/path/to/evidence"]'
export REA_INVESTIGATION_INPUT_ROOTS_JSON='["/absolute/path/to/releases"]'
rea evidence-import /absolute/path/to/evidence/bundle.json
rea evidence-export /absolute/path/to/evidence/bundle.json /absolute/path/to/evidence/canonical.json
rea compare /absolute/path/to/evidence/left.json /absolute/path/to/evidence/right.json
rea investigate-versions /absolute/path/to/releases/v1 /absolute/path/to/releases/v2 /absolute/path/to/evidence/releases.json --yes --workspace-name releases
rea analyze /absolute/path/to/releases/app.asar --approved --json
rea analyze-javascript-application /absolute/path/to/releases/app.asar --approved --json
```
JavaScript application analysis reads the selected directory or ASAR directly;
the command uses the supplied path directly:
```bash
rea analyze /absolute/path/to/releases/app.asar --json
rea analyze-javascript-application /absolute/path/to/releases/app.asar --json
```
For a directory or `.asar`, generic `rea analyze` automatically selects the
static JavaScript application provider when neither `--provider` nor
`--snapshot` is supplied. The dedicated command remains available for explicit
format and resource-limit controls. Both routes require `--approved` and an
administrator-approved investigation input root.
`--snapshot` is supplied. Both routes return the analysis and its Evidence
context inline.
If REA is registered with an MCP client, run approved `rea setup` while the
explicit investigation-root variable is set, then restart that client. Setup
copies this non-secret policy into the managed registration; changing the shell
environment alone cannot update an already-running MCP process.
`investigate-versions` inventories both versions, checkpoints their observed
Evidence, derives an artifact comparison, and records a changed-behavior report.
Both input paths must resolve beneath `REA_INVESTIGATION_INPUT_ROOTS_JSON`;
workspace files remain independently restricted by `REA_EVIDENCE_ROOTS_JSON`.
The workspace uses deterministic content identities and monotonic CAS-linked
revisions, so the same request resumes an interrupted run or reuses a completed
run without replacing earlier investigations. It currently compares static
artifact structure only; it does not execute either version, and its report
keeps every difference labeled as a behavior candidate. See
[Persistent investigation workspaces](docs/investigation-workspaces.md).
Historical source import requires a separate allowlist and never treats source as current behavioral authority:
Historical source import takes the directory directly and never treats source
as current behavioral authority:
```bash
export REA_REFERENCE_ROOTS_JSON='["/absolute/path/to/source"]'
rea import-reference-source /absolute/path/to/source
```
Exports never replace an existing file unless `--overwrite` is explicit. Imports are size/depth bounded, validate every Evidence v2 ID and manifest, and never execute bundle content.
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.
Provider-neutral analysis snapshots persist successful, immutable REA calls and
their Evidence v2 records. They are exact caches rather than Hopper databases:
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. Snapshot v1 cannot prove
those semantics and is rejected with recapture guidance. Cursor-dependent and
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, writes them with owner-only
permissions, and requires a separate approved root:
results and local paths, so REA keeps them local and writes them with owner-only
permissions. The caller supplies the snapshot path directly:
```bash
export REA_ANALYSIS_SNAPSHOT_ROOTS_JSON='["/absolute/path/to/analysis"]'
rea analyze /absolute/path/to/app --snapshot /absolute/path/to/analysis/app.json
# The same exact query can now be answered from the snapshot.
# The same exact query can be answered from that snapshot.
rea analyze /absolute/path/to/app --snapshot /absolute/path/to/analysis/app.json
```
@@ -364,25 +352,26 @@ REA handles the app analysis in steps 1–5. The agent performs step 6 with its
- Reconstruct an app's authentication, storage, update, or networking flow.
- Recover enough structure to document an undocumented format or interface.
- Trace a suspicious behavior from a string or symbol to the code that implements it.
- Run, checkpoint, resume, and reuse a content-addressed artifact investigation across two versions.
- Turn recovered behavior into product features, tests, migration notes, ports, or interoperable replacements.
- 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.
## Tool catalog for investigation
| Tool family | Count | Examples |
| ------------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Native inspection | 36 | procedures, pseudocode, assembly, strings, names, segments, callers, callees, xrefs, annotations, bounded byte reads, file-offset translation |
| Investigation workflows | 12 | `binary_overview`, `analyze_function`, `batch_decompile`, `trace_feature`, exact string-to-code lookup, bounded call paths, call graphs, Swift and Objective-C discovery |
| Native macOS utilities | 5 | Mach-O metadata, code signatures, plists, architectures, Swift demangling; Hopper-free and provenance-bearing |
| Artifact graph | 3 | bounded provider-neutral inspection, deterministic directory/ZIP/APK/IPA/MSIX/AppX/ASAR inventory, and 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 | 4 | passive root-confined observation, bounded static application mapping, and evidence-backed static/runtime reconciliation |
| JavaScript runtime | 2 | approved attach-only Node/Electron Inspector target discovery plus bounded script and execution-context observation without evaluation or instrumentation |
| Application workflows | 12 | bounded 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, deterministic obligation ledgers, and end-to-end readiness conformance |
| Workspace and observation | 19 | target lifecycle, Evidence v2 bundles, direct finite replay-machine evaluation, process/artifact/function comparison, evidence-linked residual-unknown lifecycle |
| 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 |
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.
@@ -395,32 +384,31 @@ REA is already useful for native application, browser, and Electron investigatio
- 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 v1](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 bounded static package, entrypoint, Webpack/Rspack module, import, worker, endpoint, storage, source-map, BrowserWindow, preload, contextBridge, IPC, utility-process, and native-add-on structure from an approved local directory or ASAR through `analyze_javascript_application` or `rea analyze-javascript-application`. 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.
- 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 authenticated application Evidence with explicit traversal bounds, 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 bounded 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 bounded metadata members, signatures, raw CIL hashes, limited decoded-instruction-tuple v1 hashes, 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; the v1 tuple hash does not itself 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 the shipped v1 decoded-IL commitment 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; full normalized-CIL v2 semantics, native-body bridge mapping, and an actual runtime executor remain future managed-code contracts.
- 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 bounded function dossiers with pseudocode, assembly, CFG edges, comments, calls, references, strings, and names.
- 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 v2 with artifact and provider identity, confidence, authority, limitations, and locations.
- Record every successful result as deterministic Evidence with artifact and provider identity, confidence, authority, limitations, and locations.
- Export and import evidence bundles across sessions.
- Persist automatic cross-version artifact runs as canonical, lock-protected workspaces with tamper-evident revision commitments.
- Capture approved PTY scenarios as Process Capture v4 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.
- 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 bounded, Evidence-cited direct call paths by exact address without treating missing dossiers as graph leaves.
- 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.
- With explicit `unknown_registry_approved: true`, record bounded trace/capture residuals, typed provider unavailability, and capture disagreements automatically.
- 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.
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.
@@ -438,7 +426,7 @@ 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
```
All eight browser tools expose the same Evidence v2 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.
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.
### Controlled browser scenarios
@@ -474,22 +462,19 @@ Attach-only JavaScript runtime observation is separately disabled by default:
```bash
export REA_V8_INSPECTOR_OBSERVE_ENABLED=true
export REA_V8_INSPECTOR_ENDPOINTS_JSON='["http://127.0.0.1:9229"]'
export REA_V8_INSPECTOR_FILE_ROOTS_JSON='["/absolute/path/to/app"]'
export REA_V8_INSPECTOR_ALLOWED_ORIGINS_JSON='[]'
rea list-javascript-runtime-targets http://127.0.0.1:9229 --approved --json
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 --approved --json
--runtime-kind node --json
```
REA sends only `Runtime.enable` and `Debugger.enable`. It retains bounded,
scope-authorized script locations and execution-context lifecycle events;
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).
Exact package, tool-family, provider, setup-client, public schema-version, and CLI facts are generated from source in [`docs/product-catalog.json`](docs/product-catalog.json). PR CI verifies this catalog, narrative documentation, generated schemas, and a clean TypeDoc render.
Exact package, tool-family, provider, setup-client, schema, and CLI facts are generated from source in [`docs/product-catalog.json`](docs/product-catalog.json). PR CI verifies this catalog, narrative documentation, generated schemas, and a clean TypeDoc render.
## Roadmap
@@ -502,14 +487,14 @@ REA is growing into a toolkit for understanding software across static artifacts
### Next
1. **Controlled replay conformance growth** — extend the shipped Linux extracted-module sandbox with more source-owned hostile fixtures and cross-kernel conformance; browser or Electron interaction remains a different future authority.
2. **Broader application graph evidence** — extend authenticated cross-layer traces with additional static extractors and separately approved runtime authorities.
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.
### Later
1. **Controlled application interaction** — evaluate separately authorized full-application driving without widening passive browser, Electron, or extracted-module replay authority.
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.
@@ -517,6 +502,8 @@ New providers must produce the same evidence and safety metadata as existing cap
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.
## 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.
@@ -530,7 +517,7 @@ Setup detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf,
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@2.5.1", "mcp"]
"args": ["-y", "rea-agents@3.2.1", "mcp"]
}
}
}
@@ -556,7 +543,6 @@ current session for bounded `completion/complete` suggestions; see
flowchart LR
Agent["Agent"] --> REA["REA<br/>CLI + MCP"]
Terminal --> REA
REA --> Workspace["Investigation workspace<br/>evidence + artifacts + captures"]
REA --> Session["Target-bound session router"]
Session --> Registry["Deep-provider registry<br/>deterministic selection"]
Registry --> Hopper["Hopper provider"]
@@ -573,7 +559,7 @@ flowchart LR
Artifact --> Target
```
The CLI and MCP server use the same application workflows and evidence contracts. A provider declares which capabilities it supports and the side effects those capabilities may have. Terminal commands are short-lived; an MCP session can retain an active target and evidence ledger across an investigation. Approved persistent workspaces keep canonical Evidence and resumable run checkpoints across both process and session lifetimes.
The CLI and MCP server use the same application workflows and evidence contracts. A provider declares which capabilities it supports and the side effects those capabilities may have. Terminal commands are short-lived; an MCP session can retain an active target and evidence ledger for the session.
## CLI
@@ -582,20 +568,18 @@ The agent workflow above is the easiest way to use REA. For a one-off overview f
```bash
npx -y rea-agents@latest analyze /Applications/Notes.app
npx -y rea-agents@latest inspect /Applications/Notes.app
npx -y rea-agents@latest inspect /Applications/Notes.app --detail detailed --limit 20
npx -y rea-agents@latest search /Applications/Notes.app "offline"
npx -y rea-agents@latest function /Applications/Notes.app 0x1000
npx -y rea-agents@latest xrefs /Applications/Notes.app 0x1000
npx -y rea-agents@latest trace /Applications/Notes.app "offline"
npx -y rea-agents@latest compare /absolute/path/to/left-evidence.json /absolute/path/to/right-evidence.json
npx -y rea-agents@latest investigate-versions /path/to/v1 /path/to/v2 /absolute/path/to/evidence/releases.json --yes
npx -y rea-agents@latest capabilities
npx -y rea-agents@latest providers
```
Run `npx -y rea-agents@latest --help` for direct decompilation, bounded search and
other options. `analyze` and `inspect` share the same overview workflow;
`function`, `xrefs`, and `trace` return the same Evidence v2 envelopes as MCP.
`function`, `xrefs`, and `trace` return the same Evidence envelopes as MCP.
Or install the `rea` command globally:
@@ -662,7 +646,7 @@ silent fallback.
| Status | Meaning |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `0` | The requested operation completed. Truthful unknowns, warnings, and bounded or truncated evidence remain successful results. |
| `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. |
@@ -683,11 +667,11 @@ rea inventory-artifact ./app.asar --json | jq . > inventory.json
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 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 timeouts and remediation through CLI or MCP results instead of attempting to answer UI prompts.
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 bridge calls pass through a bounded serial FIFO because Hopper's Python API runs on one dedicated thread. Queue wait counts against the caller deadline; a timeout settles the caller but does not release the wire slot until Hopper actually replies. `binary_session.analysis_activity` exposes that detached 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.
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.
Hopper's synchronous public Python calls cannot stream a partial response after the caller deadline. Use `read_function_instructions` or `rea instructions` when raw instruction orientation is enough: this path does not decompile or scan whole-program name/string inventories. If a dossier has already timed out, inspect `analysis_activity`; do not assume a raw request can enter the serial bridge until that detached Hopper call actually returns.
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 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.
@@ -704,7 +688,7 @@ 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.
Capture a scenario or compare two saved Process Capture v4 Evidence records:
Capture a scenario or compare two saved Process Capture Evidence records:
```bash
rea capture-process ./scenario.json > authority.json
@@ -714,7 +698,7 @@ 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 v4](docs/process-capture.md) for scenario
divergence. See [Process Capture](docs/process-capture.md) for scenario
fields, command-shim replay, checkpoint triggers, limits, and safety behavior.
REA installs a prebuilt PTY backend for supported macOS, Linux, and Windows
@@ -732,7 +716,7 @@ verified or absent.
## Security model
REA does not provide a hosted analysis service. Hopper and Linux Ghidra bridge communication uses authenticated private local sockets. Windows Ghidra P0 uses authenticated IPv4 loopback but does not claim named-pipe DACL or hostile-local-user isolation. Dynamic capabilities are disabled by default and require both operator policy and explicit per-call approval. Shipped providers, passive observers, and Process Capture are not security sandboxes: providers and launched targets run with the current user's permissions. Extracted JavaScript replay is a distinct Linux-only capability that fails closed unless Bubblewrap namespaces, architecture-checked seccomp, private runtime mounts, and delegated cgroup limits are available; it never inherits browser, Electron, or Process Capture authority. See [ADR-0002](docs/adr/0002-controlled-replay-authority-and-sandbox.md). Report vulnerabilities through the private process in [SECURITY.md](SECURITY.md).
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).
## FAQ
@@ -760,7 +744,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.2 and 64-bit JDK 21 installation, and loads that bridge through Ghidra's external script path after setup approval records the paths.
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.
</details>
@@ -787,7 +771,10 @@ Any agent that can run a local MCP server can use the manual configuration. Setu
## Development
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, architecture, tests, and release instructions. PR CI publishes generated API documentation as the `api-docs` workflow artifact.
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.
`npm run verify:agent` runs brandless native, JavaScript-application, managed,
and browser prompts through a real local Codex CLI. Its JSON report measures
@@ -795,7 +782,7 @@ natural MCP use, first-tool routing, repeated calls, actual Codex token usage,
completion quality, and explicit treatment of authority and unknowns.
`npm run evidence:generate` regenerates the managed conformance manifest and
Evidence v2 completion ledger from live verifier output. `npm run
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.
+17 -17
View File
@@ -65,7 +65,7 @@ npx rea-agents setup
### باستخدام وكيل برمجة — موصى به
```bash
npx rea-agents setup
npx --yes rea-agents@latest setup
```
اطلب من الوكيل إعداد REA. سيفحص جهاز Mac ويشرح ما يحتاج إلى تثبيته ويطلب الموافقة ويرشدك خلال رسائل النظام. أعد تشغيل الوكيل بعد الإعداد إذا طلب ذلك لتحميل مجموعة الأدوات الكاملة.
@@ -78,7 +78,7 @@ npx rea-agents setup
يعرض `rea setup` خطة التغييرات كاملة ويطلب التأكيد قبل تطبيقها. لا يثبت أو يحدّث Homebrew أو Node.js أو npm. إذا كان [Hopper](https://www.hopperapp.com/) مفقودًا، يقترح Setup الحزمة الرسمية. Hopper برنامج منفصل يحتاج إلى ترخيص خاص به.
إذا كان Ghidra 12.1.2 PUBLIC وJDK 21 الكامل بنواة 64 بت مثبتين مسبقًا على Linux ‏64 بت، فيمكن لـ Setup أيضًا تسجيل `GHIDRA_INSTALL_DIR` و`JAVA_HOME` الاختياري بعد الموافقة. لا ينزّل REA أو يثبت أو يعدّل Ghidra أو Java. يوفّر مزوّد Ghidra ضمن جلسة headless معزولة وللقراءة فقط الجرد، وفك الترجمة، والتجميع، وعلاقات الاستدعاء، والمراجع ذات الأنواع، وxref، وCFG، وملفات تحليل الدوال؛ وتبقى حالة الواجهة الرسومية وعمليات التعديل غير متاحة.
إذا كان Ghidra 12.1.4 PUBLIC وJDK 21 الكامل بنواة 64 بت مثبتين مسبقًا على Linux ‏64 بت، فيمكن لـ Setup أيضًا تسجيل `GHIDRA_INSTALL_DIR` و`JAVA_HOME` الاختياري بعد الموافقة. لا ينزّل REA أو يثبت أو يعدّل Ghidra أو Java. يوفّر مزوّد Ghidra ضمن جلسة headless معزولة وللقراءة فقط الجرد، وفك الترجمة، والتجميع، وعلاقات الاستدعاء، والمراجع ذات الأنواع، وxref، وCFG، وملفات تحليل الدوال؛ وتبقى حالة الواجهة الرسومية وعمليات التعديل غير متاحة.
#### التثبيت على Linux واستكشاف الأخطاء
@@ -90,7 +90,7 @@ npx rea-agents setup
```bash
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
npx rea-agents setup
npx --yes rea-agents@latest setup
```
إذا طلب macOS أو برنامج التثبيت تأكيدًا، فأكمل الخطوة ثم شغّل الأمر نفسه مرة أخرى.
@@ -159,18 +159,18 @@ rea uninstall --purge-data
## منصة أدوات التحقيق
| عائلة الأدوات | العدد | أمثلة |
| --------------------- | ----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| فحص الملفات التنفيذية | 36 | الدوال، والشيفرة شبه المصدرية، والتعليمات، والسلاسل، والأسماء، والمراجع، والتعليقات |
| التحليل المركب | 12 | النظرة العامة، وفك الترجمة الدفعي، ومخططات الاستدعاء، والمراجع، واكتشاف الأنواع |
| أدوات macOS الأصلية | 5 | بيانات Mach-O والتوقيعات وملفات plist والمعماريات وفك رموز Swift دون تشغيل Hopper |
| رسم القطع الأثرية | 3 | جرد حتمي للدلائل وZIP/APK/IPA وASAR واستخراج معاملاتي محدد صراحة |
| Managed PE/CLI | 8 | هوية PE/CLI، أعضاء البيانات الوصفية، بصمات CIL، تصريحات P/Invoke/الحدود الأصلية والتحقق منها، إسقاط رسم التطبيق، استيراد إعادة البناء من فك الترجمة، إعادة ربط الرموز، خطط ترابط وقت التشغيل ومقارنة الإصدارات |
| مراقبة المتصفح | 9 | التقاط CDP محدود الأصل وتحليل الحزم وخرائط المصدر واكتشاف WebMCP والفروق والأدلة المرئية |
| تحليل Electron | 4 | مراقبة سلبية ضمن جذور canonical، ورسم ثابت ومحدود للتطبيق، ومطابقة ثابتة/وقت تشغيل قائمة على Evidence |
| وقت تشغيل JavaScript | 2 | اكتشاف أهداف Node/Electron Inspector المعتمدة للاتصال فقط، ومراقبة محدودة للبرامج النصية وسياقات التنفيذ دون تقييم أو تجهيز |
| سير عمل التطبيقات | 12 | تتبع محدود ومقارنة إصدارات فريدة ومقارنة ثابتة لأشكال قيم إرجاع الصادرات وإعادة تشغيل وحدات معزولة ومعتمدة على Linux وتوصيف وقت التشغيل المُدار وإغلاق تغطية إعادة البناء ودفاتر التزامات حتمية والتحقق الشامل من جاهزية إعادة البناء |
| جلسة الملف التنفيذي | 19 | فتح الأهداف وحزم الأدلة وتشغيل آلة إعادة محدودة ومقارنة العمليات والقطع والدوال وسجل المجهولات المتبقية |
| عائلة الأدوات | العدد | أمثلة |
| --------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| فحص الملفات التنفيذية | 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 | فتح الأهداف وحزم الأدلة وتشغيل آلة إعادة محدودة ومقارنة العمليات والقطع والدوال وسجل المجهولات المتبقية |
## استخدام REA مع وكلاء برمجة آخرين
@@ -183,7 +183,7 @@ rea uninstall --purge-data
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@2.5.1", "mcp"]
"args": ["-y", "rea-agents@3.2.1", "mcp"]
}
}
}
@@ -258,7 +258,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.2 وJDK 21 الكامل.
نعم، يعمل REA على macOS 12+ وUbuntu 24.04+ وFedora 41+ وArch Linux ‏64 بت؛ Windows غير مدعوم حاليًا. يستخدم التحليل العميق Hopper أو مزوّد Ghidra للقراءة فقط الذي يختاره المستدعي؛ ويتطلب Ghidra على Linux الإصدار 12.1.4 وJDK 21 الكامل.
</details>
+16 -16
View File
@@ -67,7 +67,7 @@ REA は調査をバイナリ上の根拠に結び付けます。元のソース
### コーディングエージェントから(推奨)
```bash
npx rea-agents setup
npx --yes rea-agents@latest setup
```
エージェントに REA のセットアップを依頼してください。Mac を確認し、必要なインストールを説明して承認を求め、システムプロンプトを案内します。完全なツールを読み込むため再起動を求められた場合は、セットアップ後に再起動してください。
@@ -80,7 +80,7 @@ npx rea-agents setup
`rea setup` は変更計画を表示し、確認後に適用します。Homebrew、Node.js、npm をインストールまたは更新しません。[Hopper](https://www.hopperapp.com/) がない場合は公式パッケージを提案します。Hopper は別製品で、ライセンスは別途必要です。
64 ビット Linux に Ghidra 12.1.2 PUBLIC と完全な 64 ビット JDK 21 がすでにある場合、Setup は承認後に `GHIDRA_INSTALL_DIR` と任意の `JAVA_HOME` も登録できます。REA は Ghidra や Java をダウンロード、インストール、変更しません。Ghidra プロバイダーは隔離された読み取り専用 headless セッションで、インベントリ、逆コンパイル、アセンブリ、呼び出し関係、型付き参照、xref、CFG、関数 dossier を提供します。GUI 状態と変更操作は引き続き利用できません。
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 状態と変更操作は引き続き利用できません。
#### Linux のインストールとトラブルシューティング
@@ -91,7 +91,7 @@ Ubuntu 24.04+、Fedora 41+、64 ビット Arch Linux では、REA が公式の D
```bash
# 1. REA をセットアップ
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
npx rea-agents setup
npx --yes rea-agents@latest setup
```
macOS やインストーラーから確認を求められた場合は、その操作を完了してから同じコマンドをもう一度実行してください。
@@ -149,18 +149,18 @@ REA は手順 1〜5 のバイナリ解析を処理し、手順 6 はエージェ
## 調査ツールカタログ
| ツール群 | 数 | 例 |
| ---------------------- | --: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| バイナリ調査 | 36 | プロシージャ、疑似コード、アセンブリ、文字列、名前、セグメント、callers、callees、xrefs、注釈 |
| 合成解析 | 12 | `binary_overview`, `analyze_function`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, Swift / ObjC 検出 |
| macOS ネイティブ | 5 | Mach-O メタデータ、署名、plist、アーキテクチャ、Swift デマングル。Hopper 起動不要 |
| アーティファクトグラフ | 3 | ディレクトリ、ZIP/APK/IPA、ASAR の決定的一覧と明示選択されたトランザクション抽出 |
| Managed PE/CLI | 8 | PE/CLI 識別、メタデータメンバー、CIL ハッシュ、P/Invoke/ネイティブ境界宣言と検証、アプリケーショングラフ投影、逆コンパイル再構成インポート、構造的 token 再マッピング、ランタイム相関計画、バージョン比較 |
| ブラウザ観察 | 9 | origin 限定 CDP 取得、bundle/source map 解析、WebMCP 検出、セッション、capture diff、視覚証拠 |
| Electron 解析 | 4 | canonical ファイルルート内の受動観察、有界な静的アプリマッピング、Evidence に基づく静的/実行時の照合 |
| JavaScript ランタイム | 2 | 承認済みのアタッチ専用 Node/Electron Inspector ターゲット検出と、評価や計装を行わない有界なスクリプト/実行コンテキスト観察 |
| アプリケーション | 12 | 有界なクロスレイヤー追跡、一意限定のバージョン比較、静的なエクスポート戻り値形状比較、承認済み Linux 隔離 extracted-module replay、マネージドランタイム特性評価、再構築カバレッジ閉包、決定論的な義務台帳、エンドツーエンドの準備適合性検証 |
| バイナリセッション | 19 | `open_binary`、`binary_session`、証拠バンドル、有限リプレイ状態機械、プロセス/成果物/関数比較、残余未知レジストリ |
| ツール群 | 数 | 例 |
| ---------------------- | --: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| バイナリ調査 | 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`、証拠バンドル、有限リプレイ状態機械、プロセス/成果物/関数比較、残余未知レジストリ |
## 他のコーディングエージェントで使う
@@ -173,7 +173,7 @@ Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@2.5.1", "mcp"]
"args": ["-y", "rea-agents@3.2.1", "mcp"]
}
}
}
+16 -16
View File
@@ -67,7 +67,7 @@ REA는 분석을 바이너리 증거에 근거하게 합니다. 원본 소스
### 코딩 에이전트에서 시작하기(권장)
```bash
npx rea-agents setup
npx --yes rea-agents@latest setup
```
에이전트에게 REA 설정을 요청하세요. Mac을 확인하고 필요한 설치 내용을 설명한 뒤 승인을 요청하며 시스템 프롬프트를 안내합니다. 전체 도구를 불러오기 위해 재시작하라고 하면 설정 후 에이전트를 재시작하세요.
@@ -80,7 +80,7 @@ npx rea-agents setup
`rea setup`은 전체 변경 계획을 표시하고 확인 후 적용합니다. Homebrew, Node.js 또는 npm을 설치하거나 업데이트하지 않습니다. [Hopper](https://www.hopperapp.com/)가 없으면 공식 패키지 설치를 제안합니다. Hopper는 별도 라이선스가 필요한 독립 소프트웨어입니다.
64비트 Linux에 Ghidra 12.1.2 PUBLIC과 완전한 64비트 JDK 21이 이미 설치되어 있다면 Setup은 승인 후 `GHIDRA_INSTALL_DIR`과 선택 사항인 `JAVA_HOME`도 등록할 수 있습니다. REA는 Ghidra나 Java를 다운로드, 설치 또는 수정하지 않습니다. Ghidra 공급자는 격리된 읽기 전용 headless 세션에서 인벤토리, 디컴파일, 어셈블리, 호출 관계, 형식이 지정된 참조, xref, CFG 및 함수 dossier를 제공합니다. GUI 상태와 변경 작업은 계속 사용할 수 없습니다.
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 상태와 변경 작업은 계속 사용할 수 없습니다.
#### Linux 설치 및 문제 해결
@@ -91,7 +91,7 @@ Ubuntu 24.04+, Fedora 41+, 64비트 Arch Linux에서 REA는 공식 DEB, RPM 또
```bash
# 1. REA 설정
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
npx rea-agents setup
npx --yes rea-agents@latest setup
```
macOS나 설치 프로그램이 확인을 요청하면 해당 절차를 완료하고 같은 명령을 다시 실행하세요.
@@ -149,18 +149,18 @@ REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이
## 조사 도구 카탈로그
| 도구 그룹 | 수 | 예시 |
| ------------------- | --: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 바이너리 검사 | 36 | 프로시저, 의사 코드, 어셈블리, 문자열, 이름, 세그먼트, callers, callees, xrefs, 주석 |
| 합성 분석 | 12 | `binary_overview`, `analyze_function`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, Swift 및 ObjC 탐색 |
| macOS 네이티브 도구 | 5 | Mach-O 메타데이터, 코드 서명, plist, 아키텍처, Swift 디맹글링. Hopper 실행 불필요 |
| 아티팩트 그래프 | 3 | 디렉터리, ZIP/APK/IPA, ASAR 결정적 인벤토리와 명시적으로 선택한 트랜잭션 추출 |
| Managed PE/CLI | 8 | PE/CLI ID, 메타데이터 멤버, CIL 해시, P/Invoke/네이티브 경계 선언 및 검증, 애플리케이션 그래프 투영, 디컴파일 재구성 가져오기, 구조적 token 재매핑, 런타임 상관 계획, 버전 비교 |
| 브라우저 관찰 | 9 | origin 제한 CDP 캡처, bundle/source map 분석, WebMCP 검색, 세션 타임라인, capture diff 및 시각 증거 |
| Electron 분석 | 4 | canonical 파일 루트 내 수동 관찰, 제한된 정적 앱 매핑, Evidence 기반 정적/런타임 조정 |
| JavaScript 런타임 | 2 | 승인된 연결 전용 Node/Electron Inspector 대상 탐색과 평가·계측 없는 제한된 스크립트 및 실행 컨텍스트 관찰 |
| 애플리케이션 | 12 | 제한된 교차 계층 추적, 고유 일치 버전 비교, 정적 export 반환 형태 비교, 승인된 Linux 격리 extracted-module replay, 관리형 런타임 특성화, 재구성 커버리지 폐쇄, 결정론적 의무 원장, 엔드투엔드 준비 적합성 검증 |
| 바이너리 세션 | 19 | `open_binary`, `binary_session`, 증거 번들, 유한 replay 상태 머신, 프로세스·아티팩트·함수 비교, 잔여 미지수 레지스트리 |
| 도구 그룹 | 수 | 예시 |
| ------------------- | --: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 바이너리 검사 | 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 상태 머신, 프로세스·아티팩트·함수 비교, 잔여 미지수 레지스트리 |
## 다른 코딩 에이전트에서 사용하기
@@ -173,7 +173,7 @@ Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@2.5.1", "mcp"]
"args": ["-y", "rea-agents@3.2.1", "mcp"]
}
}
}
+16 -16
View File
@@ -67,7 +67,7 @@ REA 让调查始终以二进制证据为依据。它不会声称能恢复原始
### 使用编程智能体(推荐)
```bash
npx rea-agents setup
npx --yes rea-agents@latest setup
```
让智能体设置 REA。它会检查你的 Mac,说明需要安装的内容,先征得同意,并引导你处理系统提示。如果智能体要求重启以加载完整工具,请在设置后重启。
@@ -80,7 +80,7 @@ npx rea-agents setup
`rea setup` 会先显示完整变更计划并请求确认。它不会安装或更新 Homebrew、Node.js 或 npm。缺少 [Hopper](https://www.hopperapp.com/) 时,Setup 会提议安装官方软件包;Hopper 是需要单独许可证的独立软件。
如果你已在 64 位 Linux 上安装 Ghidra 12.1.2 PUBLIC 和完整的 64 位 JDK 21,Setup 也可以在批准后登记 `GHIDRA_INSTALL_DIR` 和可选的 `JAVA_HOME`。REA 不会下载、安装或修改 Ghidra 或 Java。Ghidra provider 在隔离的只读 headless 会话中提供清单、反编译、汇编、调用关系、带类型的引用、xref、CFG 与函数 dossier;GUI 状态和修改操作仍不可用。
如果你已在 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 状态和修改操作仍不可用。
#### Linux 安装与故障排除
@@ -91,7 +91,7 @@ npx rea-agents setup
```bash
# 1. 安装和配置 REA
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
npx rea-agents setup
npx --yes rea-agents@latest setup
```
如果 macOS 或安装程序要求确认,请完成提示,然后再次运行同一命令。
@@ -149,18 +149,18 @@ REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常
## 调查工具目录
| 工具类别 | 数量 | 示例 |
| ----------------- | ---: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 二进制检查 | 36 | 过程、伪代码、汇编、字符串、名称、段、调用者、被调用者、交叉引用、注释 |
| 组合分析 | 12 | `binary_overview`, `analyze_function`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`、Swift 与 ObjC 发现 |
| macOS 原生工具 | 5 | Mach-O 元数据、代码签名、plist、架构与 Swift 符号还原,无需启动 Hopper |
| 制品图谱 | 3 | 确定性目录、ZIP/APK/IPA 与 ASAR 清单,以及显式选择的事务式提取 |
| Managed PE/CLI | 8 | PE/CLI 身份、元数据成员、CIL 哈希、P/Invoke/原生边界声明与验证、应用图谱投影、反编译重建导入、结构化 token 重映射、运行时关联计划与跨版本比较 |
| 浏览器观察 | 9 | 精确来源 CDP 抓取、bundle/source map 分析、WebMCP 发现、会话时间线、capture diff 与视觉证据 |
| Electron 分析 | 4 | 限定 canonical 文件根的被动观察、有界静态应用映射,以及基于 Evidence 的静态/运行时对账 |
| JavaScript 运行时 | 2 | 经批准的只附加 Node/Electron Inspector 目标发现,以及不执行求值或插桩的有界脚本和执行上下文观察 |
| 应用工作流 | 12 | 有界跨层追踪、唯一匹配跨版本图比较、静态导出返回形状比较、经批准的 Linux 隔离 extracted-module replay、托管运行时表征、重建覆盖闭合、确定性义务账本和端到端就绪性一致性验证 |
| 二进制会话 | 19 | `open_binary`、`binary_session`、证据包、有限重放状态机、进程、制品与函数比较、残余未知项注册表 |
| 工具类别 | 数量 | 示例 |
| ----------------- | ---: | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| 二进制检查 | 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`、证据包、有限重放状态机、进程、制品与函数比较、残余未知项注册表 |
## 与其他编程智能体一起使用
@@ -173,7 +173,7 @@ Setup 会检测 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Win
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@2.5.1", "mcp"]
"args": ["-y", "rea-agents@3.2.1", "mcp"]
}
}
}
File diff suppressed because it is too large Load Diff
+190 -365
View File
@@ -9,19 +9,15 @@ import json
import hmac
import os
import re
import sre_parse
import socket
from typing import Any, Optional, Protocol, Sequence
MAX_LINE_BYTES = 10 * 1024 * 1024
MAX_READ_BYTES = 4096
BAD_ADDRESSES = (-1, 0xFFFFFFFFFFFFFFFF, None)
_selected_document = None
_rea_session_document = None
_search_inventory_cache = {}
_pseudocode_cache = {}
_hopper_api = None
MAX_SEARCH_PATTERN_LENGTH = 256
MAX_SEARCH_VALUE_LENGTH = 4096
class HopperDocument(Protocol):
@@ -69,8 +65,9 @@ class HopperApiFacade:
def _configure_hopper_api(document_provider):
"""Inject Hopper's document provider or a standalone test facade."""
global _hopper_api
global _hopper_api, _rea_session_document
_hopper_api = HopperApiFacade(document_provider)
_rea_session_document = None
def _api():
@@ -88,25 +85,53 @@ def _api():
def _session_document():
"""Find only the document opened for this authenticated REA session."""
"""Return the exact document bound to this authenticated REA session."""
global _rea_session_document
documents = _api().documents()
if _rea_session_document is not None:
return (
_rea_session_document
if any(document is _rea_session_document for document in documents)
else None
)
return _bind_session_document(documents)
def _bind_session_document(documents=None):
"""Bind this bridge to Hopper's current document for its target."""
global _rea_session_document
api = _api()
if documents is None:
documents = api.documents()
target = os.path.realpath(REA_TARGET_PATH)
for document in _api().documents():
def matches_target(document):
paths = (document.getExecutableFilePath(), document.getDatabaseFilePath())
for path in paths:
if path and os.path.realpath(path) == target:
return document
return any(path and os.path.realpath(path) == target for path in paths)
current = api.current_document()
if current is not None and matches_target(current):
_rea_session_document = current
return current
matching = [document for document in documents if matches_target(document)]
if len(matching) == 1:
_rea_session_document = matching[0]
return matching[0]
if len(matching) > 1:
raise CapabilityUnavailableError(
"The REA session document is ambiguous; Hopper did not identify the launched document"
)
return None
MAX_REGEX_BACKTRACKING_PATHS = 10000
MAX_REGEX_CANDIDATE_LENGTH = 4096
MAX_REGEX_SEARCH_WORK_UNITS = 1000000
class CapabilityUnavailableError(Exception):
"""The active Hopper build lacks one admitted public API operation."""
class InvalidRequestError(Exception):
"""Caller input failed bridge request or operation validation."""
EXHAUSTIVE_ANALYSIS_METHODS = frozenset(
(
"analyze_function",
@@ -129,6 +154,11 @@ EXHAUSTIVE_ANALYSIS_METHODS = frozenset(
def _hex(value):
if not isinstance(value, int) or isinstance(value, bool):
try:
value = int(str(value), 16)
except ValueError as error:
raise ValueError("Hopper returned a non-hexadecimal address") from error
return "0x%x" % value
@@ -149,17 +179,25 @@ def _document(name=None):
api = _api()
documents = api.documents()
if name is not None:
for candidate in documents:
if candidate.getDocumentName() == name:
return candidate
raise ValueError("Unknown Hopper document")
matches = [candidate for candidate in documents if candidate.getDocumentName() == name]
if len(matches) == 1:
return matches[0]
if len(matches) > 1:
current = api.current_document()
if current is not None and current.getDocumentName() == name:
return current
raise InvalidRequestError("Hopper document name is ambiguous")
raise InvalidRequestError("Unknown Hopper document")
if _selected_document is not None:
for candidate in documents:
if candidate.getDocumentName() == _selected_document:
return candidate
session_document = _session_document()
if session_document is not None:
return session_document
current = api.current_document()
if current is None:
raise ValueError("No Hopper document is loaded")
raise CapabilityUnavailableError("No Hopper document is loaded")
return current
@@ -168,20 +206,20 @@ def _address(document, value=None):
if value is None:
return document.getCurrentAddress()
if not isinstance(value, str):
raise ValueError("Address must be a string")
raise InvalidRequestError("Address must be a string")
try:
return int(value, 16)
except ValueError:
result = document.getAddressForName(value)
if result in BAD_ADDRESSES:
raise ValueError("Unknown Hopper address or name")
raise InvalidRequestError("Unknown Hopper address or name")
return result
def _segment(document, address):
result = document.getSegmentAtAddress(address)
if result is None:
raise ValueError("Address is outside every segment")
raise InvalidRequestError("Address is outside every segment")
return result
@@ -189,7 +227,7 @@ def _procedure(document, value=None):
address = document.getCurrentAddress() if value is None else _address(document, value)
result = _segment(document, address).getProcedureAtAddress(address)
if result is None:
raise ValueError("No procedure exists at the requested address")
raise InvalidRequestError("No procedure exists at the requested address")
return result
@@ -225,18 +263,14 @@ def _containing_procedure(document, address):
return (procedure, None) if procedure is not None else (None, "not_in_procedure")
def _instruction_addresses(procedure, limit):
def _instruction_addresses(procedure):
result = []
seen = set()
segment = procedure.getSegment()
truncated = False
for block in procedure.basicBlockIterator():
address = block.getStartingAddress()
end = block.getEndingAddress()
while address < end and address not in seen:
if len(result) >= limit:
truncated = True
return result, truncated
seen.add(address)
instruction = segment.getInstructionAtAddress(address)
if instruction is None:
@@ -246,24 +280,15 @@ def _instruction_addresses(procedure, limit):
if length <= 0:
break
address += length
return result, truncated
return result
def _procedure_references(document, params):
procedure = _procedure(document, params.get("procedure"))
direction = params.get("direction", "outgoing")
offset = params.get("offset", 0)
limit = params.get("limit", 100)
max_instructions = params.get("max_instructions", 500)
if direction not in ("incoming", "outgoing"):
raise ValueError("direction must be incoming or outgoing")
if not isinstance(offset, int) or isinstance(offset, bool) or offset < 0:
raise ValueError("offset must be a non-negative integer")
if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 500:
raise ValueError("limit must be an integer between 1 and 500")
if not isinstance(max_instructions, int) or isinstance(max_instructions, bool) or max_instructions < 1 or max_instructions > 5000:
raise ValueError("max_instructions must be an integer between 1 and 5000")
addresses, scan_truncated = _instruction_addresses(procedure, max_instructions)
raise InvalidRequestError("direction must be incoming or outgoing")
addresses = _instruction_addresses(procedure)
edges = set()
for address in addresses:
segment = _segment(document, address)
@@ -272,8 +297,7 @@ def _procedure_references(document, params):
edges.add((address, reference) if direction == "outgoing" else (reference, address))
ordered = sorted(edges)
items = []
selected = ordered[offset:offset + limit]
for source, target in selected:
for source, target in ordered:
source_procedure, _ = _containing_procedure(document, source)
target_procedure, _ = _containing_procedure(document, target)
items.append({
@@ -283,13 +307,9 @@ def _procedure_references(document, params):
"target_procedure": _procedure_identity(target_procedure) if target_procedure is not None else None,
"kind": _unavailable("Hopper's public Python API does not classify reference kinds"),
})
next_offset = offset + len(selected)
has_more = next_offset < len(ordered)
truncated = scan_truncated or has_more
return {
"procedure": _procedure_identity(procedure), "direction": direction,
"references": {"items": items, "total": None if scan_truncated else len(ordered), "returned": len(items), "truncated": truncated, "next_offset": next_offset if has_more and not scan_truncated else None},
"instructions_scanned": len(addresses), "instruction_scan_truncated": scan_truncated,
"references": items,
}
@@ -352,230 +372,39 @@ def _search_inventory(document, kind):
return inventory
def _checked_regex_paths(left, right, operation):
"""Apply one path-count operation without crossing the static work budget."""
if operation == "add":
exceeded = left > MAX_REGEX_BACKTRACKING_PATHS - right
result = left + right
else:
exceeded = right != 0 and left > MAX_REGEX_BACKTRACKING_PATHS // right
result = left * right
if exceeded or result > MAX_REGEX_BACKTRACKING_PATHS:
raise ValueError(
"Regex exceeds the %d-path backtracking budget"
% MAX_REGEX_BACKTRACKING_PATHS
)
return result
def _repeat_regex_paths(child_paths, minimum, maximum):
"""Count every bounded repetition path, including alternative child paths."""
paths = 0
repeated_paths = 1
for count in range(maximum + 1):
if count >= minimum:
paths = _checked_regex_paths(paths, repeated_paths, "add")
if count < maximum:
repeated_paths = _checked_regex_paths(
repeated_paths, child_paths, "multiply"
)
return paths
def _validate_regex_class(items):
"""Accept only constant-time character-class operations."""
allowed = {
sre_parse.CATEGORY,
sre_parse.LITERAL,
sre_parse.NEGATE,
sre_parse.RANGE,
}
if any(operation not in allowed for operation, _ in items):
raise ValueError("Regex operation is not supported by the bounded matcher")
def _validate_regex_node(node, inside_repeat=False):
"""Return capped path and step bounds for Python regex evaluation."""
leaf_operations = {
sre_parse.ANY,
sre_parse.AT,
sre_parse.CATEGORY,
sre_parse.LITERAL,
sre_parse.NOT_LITERAL,
}
forbidden = {
sre_parse.ASSERT,
sre_parse.ASSERT_NOT,
sre_parse.GROUPREF,
sre_parse.GROUPREF_EXISTS,
}
for name in ("GROUPREF_IGNORE", "GROUPREF_LOC_IGNORE", "GROUPREF_UNI_IGNORE"):
operation = getattr(sre_parse, name, None)
if operation is not None:
forbidden.add(operation)
repeat_tokens = {sre_parse.MAX_REPEAT, sre_parse.MIN_REPEAT}
possessive = getattr(sre_parse, "POSSESSIVE_REPEAT", None)
if possessive is not None:
repeat_tokens.add(possessive)
atomic = getattr(sre_parse, "ATOMIC_GROUP", None)
paths = 1
steps = 0
for operation, argument in node:
if operation in forbidden:
raise ValueError("Regex lookarounds and backreferences are not supported")
if operation in leaf_operations:
operation_paths = 1
operation_steps = 1
elif operation == sre_parse.IN:
_validate_regex_class(argument)
operation_paths = 1
operation_steps = 1
elif operation in repeat_tokens:
if inside_repeat:
raise ValueError("Nested regex repetitions are not supported")
minimum, maximum, child = argument
if maximum == sre_parse.MAXREPEAT or maximum > 1000:
raise ValueError(
"Unbounded or excessive regex repetitions are not supported"
)
child_paths, child_steps = _validate_regex_node(child, True)
operation_paths = _repeat_regex_paths(
child_paths, minimum, maximum
)
operation_steps = maximum * child_steps
elif operation == sre_parse.SUBPATTERN:
operation_paths, operation_steps = _validate_regex_node(
argument[-1], inside_repeat
)
elif operation == sre_parse.BRANCH:
operation_paths = 0
operation_steps = 0
for branch in argument[1]:
branch_paths, branch_steps = _validate_regex_node(
branch, inside_repeat
)
operation_paths = _checked_regex_paths(
operation_paths,
branch_paths,
"add",
)
operation_steps = max(operation_steps, branch_steps)
elif atomic is not None and operation == atomic:
operation_paths, operation_steps = _validate_regex_node(
argument, inside_repeat
)
else:
raise ValueError("Regex operation is not supported by the bounded matcher")
paths = _checked_regex_paths(paths, operation_paths, "multiply")
steps += operation_steps
return paths, steps
def _bounded_regex_matcher(expression, backtracking_paths, steps_per_path):
"""Create a matcher with per-candidate and cumulative work bounds."""
remaining_work = MAX_REGEX_SEARCH_WORK_UNITS
work_per_character = backtracking_paths * max(steps_per_path, 1)
def matches(value):
nonlocal remaining_work
if not isinstance(value, str):
raise ValueError("Regex candidates must be strings")
if len(value) > MAX_REGEX_CANDIDATE_LENGTH:
raise ValueError(
"Regex candidate exceeds the %d-character safety limit"
% MAX_REGEX_CANDIDATE_LENGTH
)
required_work = work_per_character * max(len(value), 1)
if required_work > remaining_work:
raise ValueError(
"Regex search exceeds the %d-unit work budget"
% MAX_REGEX_SEARCH_WORK_UNITS
)
remaining_work -= required_work
return expression.search(value) is not None
return matches
def _search_page(document, kind, params):
def _search_results(document, kind, params):
pattern = params.get("pattern")
if not isinstance(pattern, str) or not pattern or len(pattern) > MAX_SEARCH_PATTERN_LENGTH:
raise ValueError("pattern must contain between 1 and 256 characters")
if not isinstance(pattern, str) or not pattern:
raise InvalidRequestError("pattern must be a non-empty string")
mode = params.get("mode", "literal")
if mode not in ("literal", "regex"):
raise ValueError("mode must be literal or regex")
raise InvalidRequestError("mode must be literal or regex")
case_sensitive = params.get("case_sensitive", False)
if not isinstance(case_sensitive, bool):
raise ValueError("case_sensitive must be a boolean")
offset = params.get("offset", 0)
limit = params.get("limit", 100)
if not isinstance(offset, int) or isinstance(offset, bool) or offset < 0:
raise ValueError("offset must be a non-negative integer")
if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 100:
raise ValueError("limit must be an integer between 1 and 100")
raise InvalidRequestError("case_sensitive must be a boolean")
if mode == "literal":
needle = pattern if case_sensitive else pattern.casefold()
matches = lambda value: needle in (value if case_sensitive else value.casefold())
else:
try:
parsed = sre_parse.parse(pattern)
backtracking_paths, steps_per_path = _validate_regex_node(parsed)
expression = re.compile(pattern, 0 if case_sensitive else re.IGNORECASE)
except (re.error, OverflowError) as error:
raise ValueError("Invalid regex pattern") from error
matches = _bounded_regex_matcher(
expression, backtracking_paths, steps_per_path
)
raise InvalidRequestError("Invalid regex pattern") from error
matches = lambda value: expression.search(value) is not None
selected = []
total = 0
page_end = offset + limit
for item in _search_inventory(document, kind):
if not matches(item[1]):
continue
if offset <= total < page_end:
selected.append(item)
total += 1
next_offset = offset + len(selected)
has_more = next_offset < total
return {
"items": [
selected.append(item)
return [
{
"address": address,
"value": value[:MAX_SEARCH_VALUE_LENGTH],
"value_truncated": len(value) > MAX_SEARCH_VALUE_LENGTH,
"value": value,
}
for address, value in selected
],
"offset": offset,
"limit": limit,
"total": total,
"next_offset": next_offset if has_more else None,
"has_more": has_more,
}
def _page(values, offset, limit):
"""Return one deterministically address-sorted page without crossing an unbounded map."""
if not isinstance(offset, int) or isinstance(offset, bool) or offset < 0:
raise ValueError("offset must be a non-negative integer")
if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 500:
raise ValueError("limit must be an integer between 1 and 500")
ordered = sorted(values.items(), key=lambda item: int(item[0], 16))
selected = ordered[offset:offset + limit]
total = len(ordered)
next_offset = offset + len(selected)
has_more = next_offset < total
return {
"items": [{"address": address, "value": value} for address, value in selected],
"offset": offset,
"limit": limit,
"total": total,
"next_offset": next_offset if has_more else None,
"has_more": has_more,
}
]
def _unavailable(reason):
@@ -583,36 +412,6 @@ def _unavailable(reason):
return {"available": False, "reason": reason}
def _offset(params, name):
value = params.get(name, 0)
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
raise ValueError("%s must be a non-negative integer" % name)
return value
def _collection_offset(params, name):
values = params.get("collection_offset", {})
if not isinstance(values, dict):
raise ValueError("collection_offset must be an object")
value = values.get(name, 0)
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
raise ValueError("collection_offset.%s must be a non-negative integer" % name)
return value
def _bounded(items, offset, limit, total=None, scan_truncated=False):
selected = items[offset:offset + limit]
known_total = len(items) if total is None and not scan_truncated else total
has_more = offset + len(selected) < len(items)
return {
"items": selected,
"total": known_total,
"returned": len(selected),
"truncated": scan_truncated or has_more,
"next_offset": offset + len(selected) if has_more else None,
}
def _name_map(document):
result = {}
for segment in document.getSegmentsList():
@@ -639,8 +438,8 @@ def _render_instruction(segment, address):
)
def _assembly(procedure, limit=None):
"""Render bounded assembly while guarding against malformed instruction cycles."""
def _assembly(procedure):
"""Render assembly while guarding against malformed instruction cycles."""
lines = []
segment = procedure.getSegment()
seen = set()
@@ -655,8 +454,6 @@ def _assembly(procedure, limit=None):
line = _render_instruction(segment, address)
if line is not None:
lines.append(line)
if limit is not None and len(lines) >= limit:
return "\n".join(lines)
length = instruction.getInstructionLength()
if length <= 0:
break
@@ -665,37 +462,18 @@ def _assembly(procedure, limit=None):
def _read_function_instructions(document, params):
"""Read one bounded raw-instruction window without invoking decompilation."""
"""Read all raw instructions without invoking decompilation."""
procedure = _procedure(document, params.get("procedure"))
offset = _offset(params, "offset")
limit = params.get("limit", 64)
if offset > 100000:
raise ValueError("offset must not exceed 100000")
if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 500:
raise ValueError("limit must be an integer between 1 and 500")
addresses, scan_truncated = _instruction_addresses(
procedure, offset + limit + 1
)
start = min(offset, len(addresses))
end = min(len(addresses), start + limit)
addresses = _instruction_addresses(procedure)
segment = procedure.getSegment()
items = []
for address in addresses[start:end]:
for address in addresses:
line = _render_instruction(segment, address)
if line is not None:
items.append(line)
has_more = scan_truncated or end < len(addresses)
return {
"procedure": _procedure_identity(procedure),
"instructions": {
"items": items,
"total": None if scan_truncated else len(addresses),
"returned": len(items),
"truncated": has_more,
"next_offset": end if has_more else None,
},
"instructions_scanned": len(addresses),
"instruction_scan_truncated": scan_truncated,
"instructions": items,
"limitations": [
"Instruction text and ordering are Hopper-specific representations.",
"The fast path does not decompile the procedure or scan whole-program names and strings.",
@@ -704,20 +482,9 @@ def _read_function_instructions(document, params):
def _analyze_function(document, params):
"""Collect a bounded, single-pass function dossier for agent callers."""
"""Collect the complete function dossier for agent callers."""
procedure = _procedure(document, params.get("procedure"))
limit = params.get("limit", 100)
max_chars = params.get("max_pseudocode_chars", 20000)
max_instructions = params.get("max_instructions", 500)
pseudocode_offset = _offset(params, "pseudocode_offset")
assembly_offset = _offset(params, "assembly_offset")
if not isinstance(limit, int) or isinstance(limit, bool) or limit < 1 or limit > 500:
raise ValueError("limit must be an integer between 1 and 500")
if not isinstance(max_chars, int) or isinstance(max_chars, bool) or max_chars < 1 or max_chars > 100000:
raise ValueError("max_pseudocode_chars must be between 1 and 100000")
if not isinstance(max_instructions, int) or isinstance(max_instructions, bool) or max_instructions < 1 or max_instructions > 5000:
raise ValueError("max_instructions must be between 1 and 5000")
addresses, instruction_scan_truncated = _instruction_addresses(procedure, max_instructions)
addresses = _instruction_addresses(procedure)
blocks = []
all_blocks = list(procedure.basicBlockIterator())
for block in all_blocks:
@@ -732,7 +499,7 @@ def _analyze_function(document, params):
"successors": sorted(set(successors), key=lambda value: int(value, 16)),
})
pseudo = _pseudocode(document, procedure) or ""
assembly_lines = _assembly(procedure, max_instructions).splitlines() if params.get("include_assembly", False) else []
assembly_lines = _assembly(procedure).splitlines()
callers = sorted((_procedure_identity(item) for item in procedure.getAllCallerProcedures()), key=lambda item: int(item["address"], 16))
callees = sorted((_procedure_identity(item) for item in procedure.getAllCalleeProcedures()), key=lambda item: int(item["address"], 16))
comments = []
@@ -785,28 +552,17 @@ def _analyze_function(document, params):
comments.sort(key=lambda item: (int(item["address"], 16), item["kind"]))
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)))
pseudo_text = pseudo[pseudocode_offset:pseudocode_offset + max_chars]
pseudo_next = pseudocode_offset + len(pseudo_text)
def collection(name, items, scan_limited=False):
return _bounded(items, _collection_offset(params, name), limit, None, scan_limited)
return {
"procedure": {"address": _hex(procedure.getEntryPoint()), "name": _procedure_name(procedure), "classification": None, "signature": procedure.signatureString(), "locals": _procedure_locals(procedure)},
"pseudocode": {"text": pseudo_text, "total_chars": len(pseudo), "returned_chars": len(pseudo_text), "truncated": pseudo_next < len(pseudo), "next_offset": pseudo_next if pseudo_next < len(pseudo) else None},
"assembly": _bounded(
assembly_lines,
assembly_offset,
max_instructions,
None,
instruction_scan_truncated if params.get("include_assembly", False) else False,
),
"comments": collection("comments", comments, instruction_scan_truncated),
"callers": collection("callers", callers), "callees": collection("callees", callees),
"incoming_references": collection("incoming_references", incoming, instruction_scan_truncated),
"outgoing_references": collection("outgoing_references", outgoing, instruction_scan_truncated),
"referenced_strings": collection("referenced_strings", referenced_strings, instruction_scan_truncated),
"referenced_names": collection("referenced_names", referenced_names, instruction_scan_truncated),
"basic_blocks": collection("basic_blocks", blocks),
"instruction_scan": {"scanned": len(addresses), "truncated": instruction_scan_truncated},
"pseudocode": pseudo,
"assembly": assembly_lines,
"comments": comments,
"callers": callers, "callees": callees,
"incoming_references": incoming,
"outgoing_references": outgoing,
"referenced_strings": referenced_strings,
"referenced_names": referenced_names,
"basic_blocks": blocks,
"limitations": [
"Hopper's public Python API does not classify reference kinds.",
"Hopper's public Python API does not expose equivalent external or thunk classification in this dossier.",
@@ -825,6 +581,13 @@ def _dispatch(method, params):
document = _session_document()
if document is None:
return {"shutdown": True, "analysis_stopped": True, "document_closed": True}
if method == "shutdown" and not REA_OWNS_PROCESS_LIFETIME:
return {
"shutdown": True,
"analysis_stopped": not document.backgroundProcessActive(),
"document_closed": False,
"document_retained": True,
}
if document.backgroundProcessActive():
document.requestBackgroundProcessStop()
if method == "shutdown" and REA_OWNS_PROCESS_LIFETIME:
@@ -864,9 +627,9 @@ def _dispatch(method, params):
if method == "read_bytes":
length = params.get("length", 256)
if isinstance(length, bool) or not isinstance(length, int):
raise ValueError("Byte-read length must be an integer")
if length < 1 or length > MAX_READ_BYTES:
raise ValueError("Byte-read length must be between 1 and 4096")
raise InvalidRequestError("Byte-read length must be an integer")
if length < 1:
raise InvalidRequestError("Byte-read length must be at least 1")
reader = getattr(document, "readBytes", None)
if not callable(reader):
raise CapabilityUnavailableError(
@@ -900,7 +663,7 @@ def _dispatch(method, params):
or offset in BAD_ADDRESSES
or offset < 0
):
raise ValueError("Address has no authoritative file-offset mapping")
raise InvalidRequestError("Address has no authoritative file-offset mapping")
return {"address": _hex(address), "file_offset": offset}
if method == "resolve_containing_procedure":
address = _address(document, params.get("address"))
@@ -936,7 +699,7 @@ def _dispatch(method, params):
else:
result = document.getInstructionStart(max(0, target - 1))
if result in BAD_ADDRESSES:
raise ValueError("No adjacent address")
raise InvalidRequestError("No adjacent address")
return _hex(result)
if method == "list_segments":
result = []
@@ -968,24 +731,24 @@ def _dispatch(method, params):
})
return result
if method == "list_procedures":
return _page(dict(_search_inventory(document, "procedure")), params.get("offset", 0), params.get("limit", 100))
return [{"address": address, "value": value} for address, value in _search_inventory(document, "procedure")]
if method == "list_strings":
values = dict(_search_inventory(document, "string"))
requested = params.get("address")
if requested is not None:
key = _hex(_address(document, requested))
values = {key: values[key]} if key in values else {}
return _page(values, params.get("offset", 0), params.get("limit", 100))
return [{"address": address, "value": value} for address, value in values.items()]
if method == "list_names":
result = dict(_search_inventory(document, "name"))
requested = params.get("address")
if requested is not None:
key = _hex(_address(document, requested))
result = {key: result[key]} if key in result else {}
return _page(result, params.get("offset", 0), params.get("limit", 100))
return [{"address": address, "value": value} for address, value in result.items()]
if method in ("search_procedures", "search_strings"):
kind = "procedure" if method == "search_procedures" else "string"
return _search_page(document, kind, params)
return _search_results(document, kind, params)
if method.startswith("procedure_"):
procedure = _procedure(document, params.get("procedure"))
if method == "procedure_address":
@@ -1035,50 +798,111 @@ def _dispatch(method, params):
address = _address(document, params.get("address"))
document.removeBookmarkAtAddress(address)
return not document.hasBookmarkAtAddress(address)
raise ValueError("Unknown bridge method")
raise InvalidRequestError("Unknown bridge method")
def _serve_connection(connection):
"""Serve one size-bounded, capability-authenticated NDJSON connection."""
"""Serve one capability-authenticated NDJSON connection."""
file = connection.makefile("rwb")
while True:
line = file.readline(MAX_LINE_BYTES + 1)
if not line or len(line) > MAX_LINE_BYTES:
line = file.readline()
if not line:
break
request_id = None
authenticated = False
should_stop = False
try:
request = json.loads(line.decode("utf-8"))
if set(request) != {"id", "token", "method", "params"}:
raise ValueError("Invalid bridge request shape")
try:
request = json.loads(line.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError) as error:
raise InvalidRequestError("Invalid bridge request JSON") from error
if not isinstance(request, dict) or set(request) != {"id", "token", "method", "params"}:
raise InvalidRequestError("Invalid bridge request shape")
request_id = request["id"]
if not isinstance(request["token"], str) or not hmac.compare_digest(request["token"], REA_TOKEN):
if isinstance(request_id, bool) or not isinstance(request_id, int) or request_id < 0:
raise InvalidRequestError("Invalid bridge request id")
if (
not isinstance(request["method"], str) or not request["method"]
or not isinstance(request["params"], dict)
):
raise InvalidRequestError("Invalid bridge method or parameters")
if (
not isinstance(request["token"], str) or not request["token"].isascii()
or not hmac.compare_digest(request["token"], REA_TOKEN)
):
raise PermissionError("Invalid bridge capability")
authenticated = True
_write_message(file, {"id": request_id, "event": {
"type": "progress",
"phase": "hopper_bridge",
"completed": 0,
"total": 1,
"message": "Hopper bridge started request",
}})
result = _dispatch(request["method"], request["params"])
should_stop = request["method"] == "shutdown_document" or (
request["method"] == "shutdown" and not result.get("cleanup_required", False)
)
response = {"id": request_id, "result": _json_safe(result)}
safe_result = _json_safe(result)
_write_message(file, {"id": request_id, "event": {
"type": "progress",
"phase": "hopper_bridge",
"completed": 1,
"total": 1,
"message": "Hopper bridge completed request",
"terminal": True,
}})
response = {"id": request_id, "result": safe_result}
except Exception as error:
response = {"id": request_id if isinstance(request_id, int) else 0, "error": {"code": -32000, "message": str(error)[:512], "type": _diagnostic_type(error)}}
file.write((json.dumps(response, separators=(",", ":")) + "\n").encode("utf-8"))
file.flush()
diagnostic = _safe_diagnostic(error)
if authenticated:
_write_message(file, {"id": request_id, "event": {
"type": "diagnostic",
"error": diagnostic,
}})
response_id = request_id if (
isinstance(request_id, int)
and not isinstance(request_id, bool)
and request_id >= 0
) else 0
response = {"id": response_id, "error": diagnostic}
_write_message(file, response)
if should_stop:
break
file.close()
connection.close()
def _write_message(file, message):
"""Write and flush one compact bridge message."""
file.write((json.dumps(message, separators=(",", ":")) + "\n").encode("utf-8"))
file.flush()
def _diagnostic_type(error):
if isinstance(error, CapabilityUnavailableError):
return "capability_unavailable"
if isinstance(error, PermissionError):
return "authorization"
if isinstance(error, (ValueError, TypeError, KeyError)):
if isinstance(error, InvalidRequestError):
return "invalid_request"
return "bridge_exception"
def _safe_diagnostic(error):
"""Project one exception without retaining provider or credential text."""
diagnostic_type = _diagnostic_type(error)
if diagnostic_type == "capability_unavailable":
message = str(error)
elif diagnostic_type == "authorization":
message = "Invalid bridge capability"
elif diagnostic_type == "invalid_request":
message = "Invalid Hopper bridge request"
else:
message = "%s: Hopper bridge operation failed" % type(error).__name__
return {"code": -32000, "message": message, "type": diagnostic_type}
def _run():
"""Own a permission-restricted, single-client Unix socket for this bridge."""
if os.path.exists(REA_SOCKET):
@@ -1097,6 +921,7 @@ def _run():
# Hopper's public objects are bound to its dedicated Python execution thread.
# Keep dispatch on that thread; moving calls to an arbitrary worker can deadlock.
# Keep dispatch on that thread; moving calls to a worker can deadlock.
if __name__ == "__main__":
_bind_session_document()
_run()
+156
View File
@@ -0,0 +1,156 @@
import AppKit
import ApplicationServices
import CryptoKit
import ScreenCaptureKit
import Darwin
struct Request: Decodable {
var pid: Int32
var window_id: UInt32
var executable: String
var sha256: String
var launch_time: Double?
var screenshot: Bool
var accessibility: Bool
var max_nodes: Int
var action: Action?
}
struct Action: Decodable { var kind: String; var path: [Int]?; var direction: String?; var text: String? }
struct BoundaryFailure: Error { var code: String; var message: String }
func fail(_ code: String, _ message: String) throws -> Never { throw BoundaryFailure(code: code, message: message) }
func attribute(_ element: AXUIElement, _ key: String) -> CFTypeRef? {
var value: CFTypeRef?
return AXUIElementCopyAttributeValue(element, key as CFString, &value) == .success ? value : nil
}
func childCount(_ element: AXUIElement) -> Int {
var count = 0
return AXUIElementGetAttributeValueCount(element, kAXChildrenAttribute as CFString, &count) == .success ? count : 0
}
func children(_ element: AXUIElement, limit: Int = 2000) -> [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 windowBounds(_ element: AXUIElement) -> CGRect? {
guard let p = attribute(element, kAXPositionAttribute), let s = attribute(element, kAXSizeAttribute),
CFGetTypeID(p) == AXValueGetTypeID(), CFGetTypeID(s) == AXValueGetTypeID() else { return nil }
var point = CGPoint.zero; var size = CGSize.zero
guard AXValueGetValue(p as! AXValue, .cgPoint, &point), AXValueGetValue(s as! AXValue, .cgSize, &size) else { return nil }
return CGRect(origin: point, size: size)
}
func observe(_ request: Request) async throws -> [String: Any] {
guard let app = NSRunningApplication(processIdentifier: request.pid), let url = app.executableURL else {
try fail("target-mismatch", "Selected PID does not expose an application executable URL")
}
guard let canonicalPointer = realpath(url.path, nil) else { try fail("target-mismatch", "Running executable path cannot be resolved") }
defer { free(canonicalPointer) }
let canonicalExecutable = String(cString: canonicalPointer)
guard canonicalExecutable == request.executable else {
try fail("target-mismatch", "Selected PID executable \(canonicalExecutable) differs from active target \(request.executable)")
}
guard let launch = app.launchDate?.timeIntervalSince1970 else {
try fail("process-identity-unavailable", "Selected application does not expose a launch time; stable PID reuse checks are unavailable")
}
if let expected = request.launch_time, expected != launch { try fail("process-replaced", "Selected process was replaced") }
let executableSize = (try FileManager.default.attributesOfItem(atPath: url.path)[.size] as? NSNumber)?.int64Value ?? -1
guard executableSize >= 0 && executableSize <= 256 * 1024 * 1024 else { try fail("target-limit", "Running executable exceeds the 256 MiB identity-check budget") }
let executableBytes = try Data(contentsOf: url, options: .mappedIfSafe)
guard SHA256.hash(data: executableBytes).map({ String(format: "%02x", $0) }).joined() == request.sha256 else {
try fail("target-mismatch", "Running executable bytes differ from the active target digest")
}
guard let windows = CGWindowListCopyWindowInfo([.optionIncludingWindow], request.window_id) as? [[String: Any]],
windows.count == 1, let window = windows.first,
(window[kCGWindowOwnerPID as String] as? NSNumber)?.int32Value == request.pid,
(window[kCGWindowLayer as String] as? NSNumber)?.intValue == 0,
let boundsDictionary = window[kCGWindowBounds as String] as? [String: Any],
let bounds = CGRect(dictionaryRepresentation: boundsDictionary as CFDictionary) else {
try fail("window-mismatch", "Selected window is absent or belongs to another process")
}
if request.screenshot && !CGPreflightScreenCaptureAccess() {
try fail("screen-recording-denied", "Grant Screen Recording to the REA host in System Settings > Privacy & Security, then retry; no broad capture fallback is used")
}
var selected: AXUIElement?
if request.accessibility || request.action != nil {
guard AXIsProcessTrusted() else { try fail("accessibility-denied", "Grant Accessibility to the REA host in System Settings > Privacy & Security, then retry") }
let application = AXUIElementCreateApplication(request.pid)
AXUIElementSetMessagingTimeout(application, 2)
guard let axWindows = attribute(application, kAXWindowsAttribute) as? [AXUIElement] else {
try fail("accessibility-unavailable", "Selected application does not expose accessibility windows")
}
let matches = axWindows.filter { windowBounds($0) == bounds }
guard matches.count == 1 else { try fail("ambiguous-window", "Accessibility window geometry does not identify exactly one selected window; selected CG bounds \(bounds), AX candidates \(axWindows.map { String(describing: windowBounds($0)) })") }
selected = matches[0]
}
if let action = request.action, let root = selected {
var element = root
for index in action.path ?? [] {
var selectedChild: CFArray?
guard index >= 0 && index < childCount(element),
AXUIElementCopyAttributeValues(element, kAXChildrenAttribute as CFString, index, 1, &selectedChild) == .success,
let item = (selectedChild as? [AXUIElement])?.first else { try fail("element-missing", "Selected accessibility path no longer exists") }
element = item
}
var owner: pid_t = 0
guard AXUIElementGetPid(element, &owner) == .success && owner == request.pid else { try fail("element-owner-mismatch", "Selected element belongs to another process") }
let outcome: AXError
switch action.kind {
case "click": outcome = AXUIElementPerformAction(element, kAXPressAction as CFString)
case "scroll": outcome = AXUIElementPerformAction(element, (action.direction == "increment" ? kAXIncrementAction : kAXDecrementAction) as CFString)
case "key-entry": outcome = AXUIElementSetAttributeValue(element, kAXValueAttribute as CFString, (action.text ?? "") as CFString)
default: try fail("unsupported-action", "Only selected-element press, increment/decrement and text-value entry are admitted")
}
guard outcome == .success else { try fail("action-failed", "Accessibility action failed with AXError \(outcome.rawValue)") }
}
var nodes: [[String: Any]] = []; var truncated = false
if request.accessibility, let root = selected {
var pending: [(AXUIElement, [Int])] = [(root, [])]
while let (element, path) = pending.popLast() {
if nodes.count >= request.max_nodes { truncated = true; break }
let items = children(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 }
let available = max(0, request.max_nodes - nodes.count - pending.count)
let count = min(available, items.count)
if childCount(element) > count { truncated = true }
for index in (0..<count).reversed() { pending.append((items[index], path + [index])) }
}
}
var screenshot: Any = NSNull()
if request.screenshot {
guard #available(macOS 14, *) else { try fail("unsupported-host", "Selected-window screenshots require macOS 14 or later") }
let content = try await SCShareableContent.excludingDesktopWindows(true, onScreenWindowsOnly: false)
guard let scWindow = content.windows.first(where: { $0.windowID == request.window_id && $0.owningApplication?.processID == request.pid }) else {
try fail("capture-window-missing", "Selected window disappeared before capture")
}
guard bounds.width > 0 && bounds.height > 0 && bounds.width <= 100000 && bounds.height <= 100000 else { try fail("window-bounds", "Selected window has invalid or oversized geometry") }
let filter = SCContentFilter(desktopIndependentWindow: scWindow)
let configuration = SCStreamConfiguration()
let scale = min(1, 2048 / max(bounds.width, bounds.height))
configuration.width = max(1, Int(bounds.width * scale)); configuration.height = max(1, Int(bounds.height * scale))
configuration.showsCursor = false
let image = try await SCScreenshotManager.captureImage(contentFilter: filter, configuration: configuration)
guard let png = NSBitmapImageRep(cgImage: image).representation(using: .png, properties: [:]), png.count <= 8 * 1024 * 1024 else {
try fail("image-limit", "Selected-window PNG exceeded 8 MiB")
}
screenshot = ["mime_type": "image/png", "base64": png.base64EncodedString(), "sha256": SHA256.hash(data: png).map({ String(format: "%02x", $0) }).joined(), "width": image.width, "height": image.height]
}
return ["window": ["pid": request.pid, "window_id": request.window_id, "executable": request.executable, "launch_time": launch, "title": window[kCGWindowName as String] as? String ?? ""], "nodes": nodes, "truncated": truncated, "screenshot": screenshot, "gaps": request.accessibility ? [] : ["Accessibility capture was disabled"]]
}
Task { @MainActor in
do {
let request = try JSONDecoder().decode(Request.self, from: Data(CommandLine.arguments[1].utf8))
let result = try await observe(request)
let output = try JSONSerialization.data(withJSONObject: ["ok": true, "result": result], options: [.sortedKeys])
FileHandle.standardOutput.write(output)
} catch {
let failure = error as? BoundaryFailure
let output: [String: Any] = ["ok": false, "code": failure?.code ?? "capture-failed", "message": failure?.message ?? error.localizedDescription]
if let data = try? JSONSerialization.data(withJSONObject: output, options: [.sortedKeys]) { FileHandle.standardOutput.write(data) }
}
exit(0)
}
RunLoop.main.run()
@@ -278,9 +278,7 @@ The adapter returns this provider-neutral envelope:
```json
{
"schema_version": 1,
"provider": { "id": "ghidra", "version": "<concrete version>" },
"provider_profile_schema_version": 1,
"parameters": {},
"digest": "<sha256 of RFC 8785 canonical JSON excluding digest>"
}
@@ -295,8 +293,8 @@ parameters as opaque data.
A selected deep binding requires a concrete provider version. If the adapter
cannot determine it, the candidate is unavailable with `version_unresolved`;
REA does not persist a cache that merely commits `null` or "whatever is
installed". Provider identity, provider-profile schema version, normalized
parameters, or provider version changes produce a different digest.
installed". Provider identity, normalized parameters, or provider version
changes produce a different digest.
The same target, configuration, and provider must resolve the same profile over
CLI and MCP. Profile resolution must be side-effect-free and complete before
@@ -304,11 +302,10 @@ the provider imports the target.
### 7. Make snapshots provider- and profile-exact
Analysis snapshot v2 replaces Hopper loader arguments with an explicit binding
Analysis snapshots replace Hopper loader arguments with an explicit binding
commitment:
```text
snapshot_version: 2
target: digest + generic format/kind + selected architecture
binding: provider identity + analysis profile commitment
entries: exact immutable queries
@@ -321,7 +318,7 @@ A cached entry is reusable only when all of the following match exactly:
- target digest, generic format and kind, and selected architecture;
- provider ID and provider version;
- profile schema versions and profile digest;
- profile digest;
- operation and canonical parameters; and
- the existing cacheability and immutability rules.
@@ -330,20 +327,20 @@ entries from the previous binding. Auxiliary operations are cacheable only if
their provider defines an equivalent explicit profile commitment; otherwise
they remain Evidence but not snapshot entries.
Snapshot v1 is not silently upgraded. Its `loader_args` cannot prove which
engine version, defaults, or analyzer settings produced the entries. Importing
v1 returns a typed incompatibility with a recapture instruction. Its embedded
Evidence bundle may still be imported separately through the existing explicit
Evidence import flow; it is never promoted into a v2 analysis cache.
Snapshot v1 cannot be parsed as the current snapshot format. Its `loader_args`
cannot prove which engine version, defaults, or analyzer settings produced the
entries, so snapshots must be recaptured before they can be used as a cache.
An embedded Evidence bundle can still be imported separately through the
Evidence import flow; it is never promoted into an analysis cache.
### 8. Add profile commitments to new deep Evidence without rejecting legacy Evidence v2
### 8. Add profile commitments to new deep Evidence while preserving legacy records
Evidence remains schema version 2. A backward-compatible optional
`analysis_profile` field uses the canonical envelope above. Every newly created
deep-analysis observation must include it, and its provider ID and version must
match the record's provider. The field participates in the Evidence semantic ID.
The optional `analysis_profile` field uses the canonical envelope above. Every
newly created deep-analysis observation must include it, and its provider ID
and version must match the record's provider. The field participates in the
Evidence semantic ID.
Existing Evidence v2 records without `analysis_profile` remain valid and keep
Existing Evidence records without `analysis_profile` remain valid and keep
their existing IDs. Absence on a legacy deep record means the analysis profile
is unknown; REA does not infer or enrich it during import. Such evidence can be
displayed and cited but cannot establish profile compatibility, seed snapshot
@@ -459,9 +456,9 @@ snapshot's provider.
| `open_binary` and CLI structured `loaderArgs` output | Retained as a deprecated Hopper-only compatibility projection for 1.x; the analysis profile is authoritative |
| `BinaryTarget.loaderArgs` | Removed; Hopper derives them inside its adapter and commits normalized semantics in the profile |
| Legacy `.hop` input | Classified generically as an analysis database and accepted only by Hopper |
| Snapshot v1 | Rejected for cache replay with explicit recapture guidance; embedded Evidence may be imported separately |
| Existing Evidence v2 without a profile | Accepted unchanged; profile compatibility is unknown |
| New deep-analysis Evidence v2 | Must include `analysis_profile`; its semantic ID commits the field |
| Snapshot v1 | Not accepted as the current snapshot format; recapture before cache replay |
| Existing Evidence without a profile | Accepted unchanged; profile compatibility is unknown |
| New deep-analysis Evidence | Must include `analysis_profile`; its semantic ID commits the field |
| Existing status fields | Retained for 1.x with additive binding and candidate fields |
| Provider failure after selection | Returned from the selected provider; no transparent retry through another engine |
@@ -485,7 +482,6 @@ with a separately documented major contract change.
| Same target is reopened without a selector | Existing binding remains unchanged |
| Same bytes are opened with a different profile | Real provider switch and a separate snapshot partition |
| Snapshot provider or profile differs from the binding | Typed `EvidenceIntegrityError` with `profile_mismatch`; nothing is imported |
| Snapshot v1 is supplied | Typed incompatibility with recapture or explicit Evidence-import guidance |
| Artifact-only target has no deep candidate | Target opens unbound; disjoint artifact capabilities remain usable |
| Environment prefers Ghidra for an artifact-only target | Deep selection is `not_applicable`; auxiliary operations remain usable |
| Concrete deep provider is requested for artifact-only data | Typed `target_unsupported`; the request is not silently ignored |
@@ -504,7 +500,7 @@ Positive consequences:
Costs and tradeoffs:
- Multi-provider hosts must choose when more than one candidate is usable.
- Snapshot v1 cannot safely serve as a cache after this migration.
- Snapshots must be recaptured when their provider or profile binding changes.
- Providers must expose deterministic availability, support, version, and
profile resolution before they can be admitted.
- Status and error contracts gain additive structured metadata.
@@ -561,14 +557,15 @@ The first six implementation stages are shipped:
4. Ghidra availability, target/profile resolution, doctor checks, and a private
read-only headless session were admitted without declaring operations.
5. Program identity, procedures, strings, symbols, memory blocks,
address/name resolution, containing-procedure resolution, and bounded search
address/name resolution, containing-procedure resolution, and search
were admitted with exact wire schemas and real debug/stripped ELF
conformance.
6. Function metadata, persistent bounded decompilation, assembly, resolved
6. Function metadata, persistent decompilation, assembly, resolved
callers/callees, typed references, xrefs, CFG, and complete function dossiers
were admitted. A bounded per-Program queue serializes API access, and real
conformance covers x86-64 and AArch64 ELF, PE, Mach-O, stripped symbols,
targetless indirect calls, cancellation, deadlines, and cleanup.
were admitted. A serial per-Program queue keeps API access ordered without a
fixed queue length or per-operation deadline. Real conformance covers
x86-64 and AArch64 ELF, PE, Mach-O, stripped symbols, targetless indirect
calls, cancellation, and cleanup.
Future stages may deepen format and indirect-flow coverage, but must continue
to compare normalized semantics rather than provider-specific pseudocode or
@@ -168,9 +168,10 @@ requires an active permission grant and literal per-call approval bound to the
exact replay plan. Project, session, and administrator grants may satisfy the
permission-policy layer, but none replace that per-run approval.
Reproducer export is a separate host write after the sandbox has stopped. It
uses the existing bounded `evidence_write` authority and a separate export
approval; `javascript_replay` alone never writes a host file.
Reproducer export is a separate host write after the sandbox has stopped. This
ADR's root-scoped `evidence_write` authority was removed; current exports use
the path supplied by the caller, while replay still waits for complete sandbox
cleanup before writing.
### 2. Admit extracted modules only
@@ -226,7 +227,7 @@ The plan must disclose at least:
projection; and
- expected Evidence and cleanup behavior.
Execution requires both `approved: true` and the exact `plan_digest`. REA
Execution requires the exact `plan_digest`. REA
rebuilds the plan immediately before admission. Any changed file, runtime,
sandbox executable, policy, stub, input, limit, or environment commitment
returns `plan_stale` before application code starts. Approval is not transferable
@@ -321,7 +322,8 @@ Every limit has a conservative product default. The v1 plan and result commit:
- cgroup memory, swap, task, and CPU ceilings;
- Node/V8 heap ceiling as defense in depth;
- private tmpfs bytes;
- module count and aggregate bytes, case count, and input bytes;
- serialized worker-protocol bytes, aggregate module bytes, and case-input
bytes; preset-generated cases also have a fixed preflight count ceiling;
- stdout, stderr, exception, stack, and aggregate retained-output bytes; and
- structured-result depth and node count.
@@ -332,7 +334,7 @@ partial successful return.
### 7. Preserve exact provenance and the right Evidence authority
A run that reaches worker admission produces Evidence v2 with:
A run that reaches worker admission produces Evidence with:
- provider `rea-javascript-replay` and a concrete implementation version;
- operation `run_controlled_replay`;
@@ -406,7 +408,7 @@ Failure at one cleanup step does not skip the remaining steps.
The canonical replay manifest commits:
```text
schema and policy versions
plan structure and policy identity
REA/provider/runtime/sandbox executable identities and digests
module aliases, dependency graph, bytes digests, and source references
stub IDs, versions, source digests, and caller-provided stub digests
@@ -189,7 +189,7 @@ or a CLR load. Complete-coverage claims require every applicable bounded region
to have been admitted; otherwise coverage is partial, unknown, or unavailable
with exact offsets and reasons.
The input path remains under REA's approved investigation roots. Local artifact
The caller selects the local input path directly. Local artifact
paths, digests, MVIDs, tokens, malformed offsets, and analysis metadata are
actionable diagnostics and are not secrets. Credentials, authorization data,
and genuine secrets remain redacted.
@@ -203,7 +203,7 @@ artifact: canonical path + byte length + SHA-256
component: container identity + component path/name + component SHA-256, when applicable
classification: container + runtime family + implementation form + architecture
assembly/module: simple name + version + culture + public-key identity + module name + MVID
producer: provider ID + exact version + parser/profile schema + profile digest
producer: provider ID + exact version + analysis-affecting parameters + profile digest
```
An assembly-qualified name does not replace the artifact digest or MVID.
@@ -218,7 +218,7 @@ declaring type + normalized CLI signature
build-local metadata token
RVA/file extent when present
raw CIL byte length + SHA-256 when the body is admitted
member-result schema version + decoded-instruction-tuple SHA-256 when CIL is valid
decoded-instruction-tuple SHA-256 when CIL is valid
separate max-stack, locals-token, and exception-region observations
```
@@ -232,9 +232,9 @@ The caller-visible field remains named `normalized_il_sha256`, but its shipped
v1 meaning is deliberately narrower than a canonical semantic CIL identity.
For a completely decoded method, REA serializes the instruction-order array of
`[opcode, operand_kind, operand]` tuples with JavaScript `JSON.stringify`,
encodes that text as UTF-8, and reports its lowercase SHA-256. The member
result's `schema_version: 1` scopes this convention; there is no independent
normalized-CIL algorithm-version field in v1.
encodes that text as UTF-8, and reports its lowercase SHA-256. The tuple
projection is defined here; the member result carries only the described
normalized digest.
Operands retain the current decoder projection: metadata and user-string
operands are build-local lowercase hexadecimal tokens, scalar branch operands
@@ -258,8 +258,8 @@ 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, the
member-result schema version, and all competing candidates. V1 cannot cite a
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.
### 5. Separate five evidence layers
@@ -386,7 +386,7 @@ requires its own threat model and admission decision.
- Managed addresses distinguish file offsets, RVAs, native virtual addresses,
metadata tokens, and CIL offsets. They are not interchangeable.
- Cache compatibility is exact on artifact/component commitment, member-result
schema, parser profile, and producer version. For shipped v1,
fields, parser profile, and producer version. For shipped v1,
`normalized_il_sha256` has no independent algorithm-version field.
- Provider-neutral workflows may compose canonical managed findings with the
one native binding, but every source operation retains its actual producer.
+18
View File
@@ -0,0 +1,18 @@
# Apple Application Inventory Projection
REA can project one or more authenticated `inventory_artifact` Evidence records
for the same IPA into an Apple application inventory. The projection is
execution-free: it reports exact paths, content hashes, detected formats,
runtime-family hints, and path-based bridge hypotheses. It does not parse plist
or CMS semantics or claim observed runtime calls.
The result includes every component in each inventory category and every
JavaScript-to-native bridge candidate pair. It has no caller-selected component
budget, prefix truncation, or omitted-count fields. Duplicate inventory pages
are merged by artifact identity and occurrence path; their Evidence IDs remain
attached as source Evidence.
Coverage is `complete-within-inventory` when the supplied pages reconstruct the
complete authenticated inventory, and `partial` otherwise. A partial result
states that absence is unknown. Component and bridge-candidate arrays are still
the complete projection of the inventory that was supplied.
+3 -5
View File
@@ -16,8 +16,7 @@ graph TD
APP["src/application/<br/>Shared CLI/MCP workflows<br/>enhanced analysis and investigations"]
SESSION["SessionProviderRouter + BinarySession<br/>one immutable deep binding per target<br/>disjoint auxiliary routing"]
REGISTRY["AnalysisProviderRegistry<br/>sorted deep candidates<br/>deterministic selection, no fallback"]
WORKSPACE["Investigation workspace store<br/>approved roots, canonical JSON<br/>atomic CAS revisions and locks"]
JS_RECON["JavaScript artifact reconstruction<br/>bounded directory/ASAR reads<br/>AST-only bundle projection"]
JS_RECON["JavaScript artifact reconstruction<br/>directory/ASAR reads<br/>AST-only bundle projection"]
end
subgraph "Provider Layers"
@@ -29,7 +28,7 @@ graph TD
end
subgraph "Provider Process Foundation"
PROCESS["src/process/<br/>owned process groups, private roots<br/>deadlines, bounded diagnostics, cleanup"]
PROCESS["src/process/<br/>owned process groups, private roots<br/>deadlines, bounded file reads and diagnostics, cleanup"]
end
subgraph "Contracts Layer"
@@ -37,7 +36,7 @@ graph TD
end
subgraph "Domain Layer"
DOMAIN["src/domain/<br/>Pure side-effect-free modules<br/>evidence, comparisons, application graphs, workspace commitments"]
DOMAIN["src/domain/<br/>Pure side-effect-free modules<br/>evidence, comparisons, application graphs"]
end
subgraph "Configuration"
@@ -59,7 +58,6 @@ graph TD
REGISTRY --> HOPPER
REGISTRY --> GHIDRA
SESSION --> AUX
APP --> WORKSPACE
APP --> JS_RECON
APP --> INSTALL
SERVER --> CONTRACTS
+46 -46
View File
@@ -1,19 +1,21 @@
# Website observation with CDP
REA can attach to a user-owned Chrome-family browser through the Chrome DevTools Protocol (CDP) and produce bounded Evidence v2 about an existing page. This is a passive reverse-engineering capability, not a general browser automation or remote-control surface.
REA can attach to a user-owned Chrome-family browser through the Chrome DevTools Protocol (CDP) and produce bounded Evidence about an existing page. This is a passive reverse-engineering capability, not a general browser automation or remote-control surface.
## Shipped surfaces
- `list_browser_targets` / `rea list-browser-targets` discovers page targets whose current URL matches an approved exact origin.
- `inspect_web_page` / `rea inspect-web-page` captures bounded 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.
- `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.
- `observe_web_session` / `rea observe-web-session` arms a bounded window 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 inventory page registrations. REA never exposes `WebMCP.invokeTool`.
- `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, bounded, content-addressed visible-viewport PNG.
- `capture_web_screenshot` / `rea capture-web-screenshot` returns an explicitly approved, 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 v2 provenance.
- MCP session results are retained as `rea://evidence/{evidenceId}` resources. One-shot CLI output is not retained by a long-lived REA session.
- 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.
Electron `file://` pages use a separate provider, permission capability, and root model; see [electron-observation.md](electron-observation.md).
Existing static application Evidence and passive web/Electron captures can be
@@ -47,7 +49,7 @@ export REA_BROWSER_ALLOWED_ORIGINS_JSON='["http://127.0.0.1:3000"]'
`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. A request still requires `approved: true`; configuration alone does not make a tool call implicit.
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:
@@ -64,9 +66,8 @@ rea policy explain browser_observe \
Configured origins are used by default. `--allowed-origins` can request a narrower set that still fits the configured ceiling.
```bash
rea list-browser-targets http://127.0.0.1:9222 --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 \
--approved \
--observation-ms 1000 \
--json
```
@@ -75,34 +76,33 @@ Static bundle analysis and a user-driven observation window are separate operati
```bash
rea analyze-web-bundle http://127.0.0.1:9222 TARGET_ID \
--approved --source-capture-approved --json
--json
rea observe-web-session http://127.0.0.1:9222 TARGET_ID \
--approved --observation-ms 10000 --json
--observation-ms 10000 --json
rea discover-webmcp-tools http://127.0.0.1:9222 TARGET_ID \
--approved --json
--json
```
Accessibility names/descriptions, console primitive text, JSON body shapes, WebSocket shapes, script content, and storage key names require separate opt-ins. Console/body/WebSocket capture additionally requires its matching approval flag:
Accessibility names/descriptions, console primitive text, JSON body shapes, WebSocket shapes, script content, and storage key names are selected independently:
```bash
rea inspect-web-page http://127.0.0.1:9222 TARGET_ID \
--approved \
--include-accessibility-text \
--include-console-text --console-text-approved \
--include-json-body-shapes --json-body-schema-approved \
--include-websocket-shapes --websocket-shape-approved \
--include-console-text \
--include-json-body-shapes \
--include-websocket-shapes \
--include-script-sources \
--include-storage-keys --include-storage-fingerprints \
--json
```
Accessibility text and script content may contain application secrets and become part of the returned Evidence. Accessibility and console text have independent per-field and aggregate UTF-8 byte limits; credential-shaped console substrings are redacted. JSON and WebSocket captures retain paths, types, counts, and truncation only, never values or examples. Storage values remain redacted even when key-name capture is approved. `--include-storage-fingerprints` separately approves SHA-256 identity/value fingerprints, which support comparison only when cookies, DOM storage, IndexedDB, and Cache Storage were all captured completely.
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.
Screenshot pixels require both `--approved` and `--screenshot-approved`:
Screenshot capture returns the complete visible viewport as an inline image artifact:
```bash
rea capture-web-screenshot http://127.0.0.1:9222 TARGET_ID \
--approved --screenshot-approved --json
--json
```
## MCP input
@@ -111,22 +111,16 @@ rea capture-web-screenshot http://127.0.0.1:9222 TARGET_ID \
{
"cdp_endpoint": "http://127.0.0.1:9222",
"allowed_origins": ["http://127.0.0.1:3000"],
"target_id": "TARGET_ID_FROM_LIST_BROWSER_TARGETS",
"approved": true,
"observation_ms": 500,
"include_accessibility_text": false,
"include_console_text": false,
"console_text_approved": false,
"include_json_body_shapes": false,
"json_body_schema_approved": false,
"include_websocket_shapes": false,
"websocket_shape_approved": false,
"include_script_sources": false,
"include_storage_keys": false,
"include_storage_fingerprints": false
"target_id": "TARGET_ID_FROM_LIST_BROWSER_TARGETS"
}
```
The omitted observation window and capture options use conservative defaults.
Set an option only when the investigation needs that additional content.
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.
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`.
@@ -137,28 +131,34 @@ REA removes sensitive values before normalized event data is retained:
- 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.
- 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, but names and descriptions require `include_accessibility_text: true` and remain byte-bounded.
- 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.
- Request/response bodies are not requested or parsed by default. With independent approval, only allowed-origin JSON media types are read within per-body and aggregate budgets, converted immediately to value-free property paths/types, and discarded. `Network.getResponseBody` is never sent without that approval.
- Console observations with an approved stack source retain call type, argument types, timestamp, and redacted source location. With independent approval, only already-delivered primitive values are retained after credential redaction and byte bounds; objects, getters, and remote properties are never expanded.
- WebSocket observations retain direction, opcode, and payload byte length. With independent approval, bounded text frames are classified as text or 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` approval. 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 becomes a self-verifying `rea://web-content/sha256/...` artifact subject to per-script and aggregate byte limits.
- 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.
- 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.
Retained workers include bounded opener-target and parent-frame IDs when CDP
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.
## Completeness and limits
The default observation window is 500 ms and the maximum is 10 seconds. Frames (200), DOM nodes (2,000), accessibility nodes (2,000), scripts (200), resources (2,000), workers (500), and each storage key/name inventory (1,000) have conservative defaults and caller-visible hard bounds. Script source bytes, network events, console events, and WebSocket events are bounded separately. The CLI exposes the same limits as kebab-case options such as `--max-frames` and `--max-storage-keys`.
The default inspection window is 500 ms; callers choose its duration. The
capture returns all in-scope frames, DOM and accessibility nodes, scripts,
resources, workers, storage names, and observed network, console, and WebSocket
events. Full selected text, script sources, and JSON shapes are returned inline.
The result distinguishes
`complete_within_window`, `policy_filtered`, `attach_limited`, and `truncated`
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.
Every result distinguishes `complete_within_window`, `policy_filtered`, `attach_limited`, and `truncated` coverage. It names affected sections and reports sparse excluded-item and per-event-type dropped counts rather than implying exhaustive capture. Disallowed-origin entries are filtered before collection-limit accounting, so they cannot consume an approved origin's output budget.
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 each command have a 5-second timeout. Version discovery is capped at 64 KiB, target discovery at 2 MiB and 1,000 targets, each WebSocket message at 16 MiB, and correlated pending commands at 128. 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, no credentials/cookies/referrer, per-map and aggregate byte limits, a total map-count limit, v3 validation, and bounded generated/original mappings. Source-map results report requested, processed, and dropped counts rather than hiding budget truncation.
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.
## Non-goals and threat model
+26 -12
View File
@@ -1,11 +1,21 @@
# Browser scenario contract
`capture_browser_scenario` accepts the versioned provider-neutral
`capture_browser_scenario` accepts the provider-neutral
`browserScenarioSchema` through both MCP and the
`capture-browser-scenario INPUT_JSON` CLI command. Version 1 is declarative and
bounded: it admits a fixed action vocabulary, exact HTTP(S) origins,
deterministic browser settings, explicit storage and request replay, and finite
capture limits. `INPUT_JSON` may be inline JSON or a JSON file path.
`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
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
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,
@@ -30,14 +40,18 @@ 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 result is Evidence v2 with an initial state followed by one record per
The result is Evidence with an initial state followed by one record per
declared action. Each step reports action status, elapsed time, sanitized URLs,
event bounds, and independently typed capture states for screenshot, DOM,
accessibility, URL, history, and storage. Console, page-error, network,
WebSocket, frame, worker, popup, and cancelled-download events are attributed to
steps and bounded by the scenario limits. Missing and truncated sections remain
explicit and make the capture ineligible for equality claims. Attach-mode
captures also declare the unavoidable pre-attach event gap.
steps and all observed events and requested artifacts are returned inline.
There are no event, frame, worker, popup, WebSocket, DOM-node,
accessibility-node, storage-entry, screenshot-count, or cumulative
metadata-byte caps. Requested artifacts and observed events are returned
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:
@@ -75,6 +89,6 @@ capture fields and are preserved in the result.
Each aligned step reports changed, unchanged, or unknown. Changed and unknown
action, screenshot, DOM, accessibility, URL, history, storage, and event
artifacts carry their normalized before/after digests and capture states.
`max_changes` bounds retained artifact records while total and omitted counts
remain visible. Missing, truncated, or mismatched capture coverage remains
unknown instead of being treated as equality.
Every changed or unknown artifact record is returned inline for each aligned
step. Missing, truncated, or mismatched capture coverage remains unknown
instead of being treated as equality.
+2 -2
View File
@@ -79,8 +79,8 @@ 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 requires both
`approved: true` inside that object and separate `evidence_write` authority.
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
+68 -16
View File
@@ -1,11 +1,10 @@
# 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. Electron observation is separate from website observation because filesystem roots, not HTTP origins, define its authority.
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.
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
`REA_INVESTIGATION_INPUT_ROOTS_JSON`; it does not attach to CDP. Runtime
observations and static inferences are never silently treated as the same fact.
Use the separate
[`reconcile_javascript_runtime`](javascript-runtime-reconciliation.md) workflow
@@ -17,43 +16,96 @@ The capability is disabled by default:
```bash
export REA_ELECTRON_OBSERVE_ENABLED=true
export REA_ELECTRON_CDP_ENDPOINTS_JSON='["http://127.0.0.1:9223"]'
export REA_ELECTRON_FILE_ROOTS_JSON='["/Applications/Example.app/Contents/Resources"]'
```
Endpoints accept only explicit-port loopback HTTP URLs. Roots are canonicalized by the shared permission authority. Each target, frame, script, and resource is independently converted from a hostless `file://` URL to a real path and checked after symlink resolution. UNC hosts, percent-encoded path separators, nonexistent paths, and root escapes are rejected.
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.
## Workflow
`list_electron_targets` returns every eligible target served by the supplied endpoint in one inline array.
For the MCP follow-up call, pass the same literal-loopback endpoint and the selected target ID to `inspect_electron_page`. REA rediscovers targets and validates the ID against the live endpoint before inspection.
```bash
rea list-electron-targets http://127.0.0.1:9223 --approved --json
rea list-electron-targets http://127.0.0.1:9223 --json
rea inspect-electron-page http://127.0.0.1:9223 TARGET_ID \
--approved --observation-ms 100 --json
--observation-ms 100 --json
```
Script content is excluded by default. Capturing it requires both flags and remains subject to per-script and aggregate byte budgets:
Script content is excluded by default. Requesting it includes every authorized script source:
```bash
rea inspect-electron-page http://127.0.0.1:9223 TARGET_ID \
--approved \
--include-script-sources \
--source-capture-approved \
--json
```
The normalized result contains canonical local paths, bounded frame and DOM
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
retains its execution-context frame ID when CDP supplies one. The capture also
inventories authorized worker, service-worker, and shared-worker targets with
bounded opener-target and parent-frame IDs. Worker discovery uses passive
validated opener-target and parent-frame IDs. Worker discovery uses passive
target metadata; REA does not attach to or execute code in those targets.
The default worker limit is 500 and the caller-visible maximum is 5,000. Like
every target, frame, script, and resource, a worker URL must resolve beneath an
approved canonical root before it is retained. Relationship IDs improve
Collection counts and aggregate script-source bytes are not capped. Like every
target, frame, script, and resource, a worker URL must resolve to a local
file before it is retained. Relationship IDs improve
attribution but do not prove which static module started a worker or that its
work completed.
Inspection does not retain DOM values, execute renderer code, navigate, click,
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,
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:
```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.
The CLI and MCP surfaces accept the same schema. For a JSON request file:
```bash
rea capture-electron-scenario scenario.json --json
```
The result records action status and targets, correlated app/window/WebContents,
preload, session, navigation, shell, permission, popup, download, protocol,
native-addon, process, and IPC timeline events. IPC channels are capped at
1,024 characters and argument-shape metadata at 32 entries; values are never
retained. Renderer crash/restart and deep-link actions are synthetic
scenario controls; their attempted and observed outcomes remain in the
timeline. The active hook blocks and records external shell/navigation,
permission, download, popup, updater, and OS-integration effects. The timeline
is explicitly partial when attachment starts after application activity. The
owned experiment has an internal 60-second deadline and a 5-second per-action
timeout so hung work reaches process cleanup; these are lifecycle timeouts, not
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
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.
-39
View File
@@ -661,45 +661,6 @@
"required": ["code", "category", "message", "retryable", "remediation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"code": { "type": "string", "const": "outside_approved_root" },
"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",
"propertyNames": { "type": "string" },
"additionalProperties": { "$ref": "#/$defs/__schema0" }
}
},
"required": ["code", "category", "message", "retryable", "remediation"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
+105 -26
View File
@@ -7,7 +7,7 @@ REA separates installing its CLI from configuring external software and agents.
The recommended setup entrypoint is:
```bash
npx rea-agents setup
npx --yes rea-agents@latest setup
```
If npm asks to download and run the package, that approval applies only to the
@@ -15,12 +15,28 @@ 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.
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.
For an intentional rollback, make the package request explicit:
```bash
npm exec --yes --package=rea-agents@2.4.0 -- rea setup
```
Setup continues to pin persistent MCP registrations to the exact version that
performed setup. Running current setup later migrates unversioned or older
managed registrations through the normal reviewed setup transaction.
REA supports Node.js 22.19+ and 24.11+ (including newer releases). It uses the npm already paired with that runtime and never upgrades Node.js, npm, or Homebrew.
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 `npx rea-agents setup` for the guided setup journey,
`npx -y rea-agents@latest` for unattended one-off commands, or install globally
on the shell `PATH`. Use the explicit 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.
The optional curl wrapper installs only the global npm package:
@@ -133,6 +149,16 @@ and digest, validates the application bundle, and atomically installs it to
operator can choose its demo mode or activate an existing license. Homebrew and
administrator access are not used.
The Hopper launcher action used by REA creates a document from an executable;
its supported command-line interface does not attach to an already-open
document. While another live REA session owns the same target and loader
profile, a second session reports the owning run ID instead of opening a
duplicate document. Closing the owning REA session releases this guard; Hopper
keeps its document open. A later REA session can therefore open another
document for the same target. REA does not currently identify, focus, or reuse
that existing GUI document, and the supported launcher exposes no attach or
reuse action for it.
On supported Linux distributions, approved setup verifies Hopper's official
`.deb`, `.rpm`, or Arch package before invoking the native package manager.
REA runs the supported demo build on a private Xvfb display and selects Hopper's
@@ -142,24 +168,45 @@ desktop display. Unattended package-manager access requires
mount, REA first verifies the conflict and then uses an unprivileged user and
mount namespace with a private mode-1777 tmpfs over that directory only. The
host mount and the rest of `/tmp` remain unchanged; this fallback never invokes
`sudo`. `rea doctor --provider hopper --detail full --json` reports the selected
`sudo`. `rea doctor --provider hopper --json` reports the selected
strategy and both host and effective mount facts.
### Hopper in CI
REA's unattended Linux path is validated against Hopper's offered demo mode.
A paid license is not required for that path, and REA does not read, install, or
automate license credentials. Licensed Hopper installations remain supported,
but license activation is an operator-owned prerequisite rather than part of
REA setup.
macOS requires Hopper's first-run UI to be completed in the same user session
that will run REA: choose the demo mode or activate an existing license before
starting an unattended job. Ephemeral macOS runners therefore need a
pre-provisioned user session or a deliberate interactive bootstrap step.
When Hopper cannot start in CI, run
`rea doctor --provider hopper --json` in the failing runner.
Structured failures distinguish private-display dependencies, an unsupported
demo dialog or build, process-ownership conflicts, and an early lifecycle exit.
Apply the reported remediation rather than exposing the runner's desktop,
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.2 release and a 64-bit full JDK 21. An experimental
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 bounded inventory/name/search operations, and nine function-analysis
operations covering metadata, decompilation, assembly, resolved calls, typed
references, xrefs, CFG, and dossiers. GUI state and analysis mutations remain
unavailable through Ghidra.
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.
Extract Ghidra and install the JDK outside REA, then export absolute paths:
```bash
export GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra_12.1.2_PUBLIC
export GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra_12.1.4_PUBLIC
export JAVA_HOME=/absolute/path/to/jdk-21 # optional if java/javac are on PATH
rea doctor --json
rea setup
@@ -168,7 +215,7 @@ rea setup
PowerShell configuration for Windows uses the same non-secret paths:
```powershell
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.2_PUBLIC"
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
$env:JAVA_HOME = "C:\Program Files\Java\jdk-21"
rea doctor --json
rea providers --json
@@ -188,30 +235,35 @@ environment entry in its plan, writes only after approval, and never downloads,
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`, a
per-file analysis timeout, CPU and heap bounds, and its packaged Java bridge via
`-scriptPath`; it never opens an existing user project. Linux uses a
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.
Operations begin only after default auto-analysis completes. A timeout fails
the open rather than exposing partial analysis. One session contains exactly
one imported Program; use `provider_id: "ghidra"`, `--provider ghidra`, or
Operations begin only after default auto-analysis completes. The provider
startup deadline fails the open rather than exposing partial analysis. One
session contains exactly one imported Program; use `provider_id: "ghidra"`, `--provider ghidra`, or
`REA_ANALYSIS_PROVIDER=ghidra` when both Hopper and Ghidra support the target.
One persistent decompiler is owned by the Program, Ghidra API calls cross a
bounded 32-request serial queue, and each native decompile is limited to 30
seconds. Unresolved computed calls remain unknown, reference-kind provenance is
preserved, and provider-specific pseudocode is never treated as original source
or Hopper-equivalent text.
One persistent decompiler is owned by the Program, and a serial queue keeps
Ghidra API calls on the owning Program thread without a fixed queue length.
Operations run until a result, caller cancellation, or provider shutdown; there
is no fixed per-operation or response-size ceiling. Unresolved computed calls
remain unknown, reference-kind provenance is preserved, and provider-specific
pseudocode is never treated as original source or Hopper-equivalent text.
Run `GHIDRA_INSTALL_DIR=... npm run verify:ghidra` from a source checkout to
compile and analyze source-owned x86-64 debug/stripped ELF, AArch64 ELF,
x86-64 PE, and x86-64 Mach-O conformance fixtures. The verifier expects `cc`,
`clang`, and `lld-link` on `PATH`; `REA_CC`, `REA_CLANG`, and `REA_LLD_LINK` can
select alternate command paths.
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.
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
`clang`, LLD, and `lld-link` on `PATH`; `REA_CLANG` and `REA_LLD_LINK` select
alternate command paths. Missing cross-target tooling does not block the
host-native Ghidra acceptance lane.
On a controlled Windows x64 runner, use
`npm run verify:ghidra:windows`. The verifier generates a deterministic native
@@ -221,3 +273,30 @@ target/snapshot/import digest identity, and requires 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.
## MCP Registry
REA is published in the official MCP Registry as `io.github.morluto/rea`. Registry
clients can discover the server and install the existing public `rea-agents` npm
package; the Registry entry does not introduce a second distribution artifact.
For a client that supports Registry discovery, search for `io.github.morluto/rea`
and select the npm package. The published metadata launches the existing stdio
server through `npx` with the `mcp` command.
For a client that requires manual configuration, use:
```json
{
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@latest", "mcp"]
}
}
}
```
Use an exact `rea-agents@VERSION` in the arguments when a reproducible client
configuration is required. `rea setup` writes the same package-runner shape and
pins it to the exact version that performed setup.
-124
View File
@@ -1,124 +0,0 @@
# Persistent investigation workspaces
REA can run a deterministic artifact investigation across two versions and
persist its Evidence beyond a CLI process or MCP session. The CLI and MCP use
the same application workflow.
## CLI
First allow the directory that will own the workspace, then explicitly approve
the write:
```bash
export REA_EVIDENCE_ROOTS_JSON='["/absolute/path/to/evidence"]'
export REA_INVESTIGATION_INPUT_ROOTS_JSON='["/absolute/path/to/releases"]'
rea investigate-versions \
/absolute/path/to/version-1 \
/absolute/path/to/version-2 \
/absolute/path/to/evidence/releases.json \
--workspace-name releases \
--yes
```
The two versions may be regular artifacts or directories supported by the
artifact graph provider. Both must resolve beneath an explicitly configured
`REA_INVESTIGATION_INPUT_ROOTS_JSON` root. The workspace parent must already
exist beneath `REA_EVIDENCE_ROOTS_JSON`. REA never creates an allowlisted root
implicitly.
Use `--expected-revision N` when an external coordinator needs an explicit
compare-and-swap guard. Traversal, byte, page, and comparison limits are also
available as CLI options.
After a completed run returns its stable run ID, `--replay-run-id RUN_ID`
selects that exact result without reading or rescanning either version input.
The workspace still requires read authority. REA rejects a selector that is
missing, incomplete, or inconsistent with the requested paths and controls.
## MCP
`find_changed_behavior` retains its existing aggregation mode. Its additive
`investigation_run` mode starts or resumes the same persistent workflow:
```json
{
"investigation_run": {
"approved": true,
"workspace_path": "/absolute/path/to/evidence/releases.json",
"workspace_name": "releases",
"left_path": "/absolute/path/to/version-1",
"right_path": "/absolute/path/to/version-2",
"options": {
"max_entries": 10000,
"max_total_bytes": 1073741824,
"max_entry_bytes": 268435456,
"max_compression_ratio": 1000,
"max_depth": 20,
"max_path_bytes": 4096,
"page_size": 500,
"change_limit": 500
}
}
}
```
Exactly one of `comparisons` or `investigation_run` is accepted. The MCP tool
inventory remains unchanged.
## Checkpoints and reuse
One run has four ordered stages:
1. Inventory the left version.
2. Inventory the right version.
3. Compare the complete artifact Evidence page sets.
4. Derive the changed-behavior report.
The inventory checkpoint is written first, followed by the comparison and final
report checkpoints. A process interrupted after a checkpoint can resume from
the last complete stage. A completed run is reused when both graph commitments
and every bounded option are identical.
By default, REA rescans the version inputs so changed content creates a new
run. For an explicit cache-only replay, add the returned `run_id` as
`investigation_run.replay_run_id`. REA validates the workspace revision and
name, the complete run, its exact paths and controls, and every linked Evidence
record before it skips input-read authorization.
The run ID is a SHA-256 commitment to both root and graph digests plus the
normalized options. Local paths and timestamps do not affect that identity.
Changing either artifact or a budget creates a new run in the same workspace;
it does not overwrite the earlier run.
## Integrity and concurrency
A workspace contains a canonical Evidence v2 bundle plus run manifests. Each
workspace revision has a deterministic digest and points to the prior revision
digest. Parsing verifies:
- the workspace, run, Evidence, graph, and revision identities;
- canonical ordering and unique run IDs;
- every run-to-Evidence reference;
- stage and completion-state consistency.
Writes are canonical JSON, atomic, fsynced, and mode `0600`. REA rejects
workspace paths outside `REA_EVIDENCE_ROOTS_JSON`, symlink destinations,
artifact inputs outside `REA_INVESTIGATION_INPUT_ROOTS_JSON`,
oversized or deeply nested JSON, stale expected revisions, and concurrent lock
holders. Lock or CAS conflicts fail without mutating the workspace; callers may
retry the same idempotent request.
## Truth boundary
The automatic run currently observes shipped artifact structure. Artifact
changes are static behavior candidates, not runtime observations or causal
claims. Without controlled process comparison Evidence, `behavior_status`
remains `unknown` even when static differences are present. Provider
limitations, incomplete comparison pages, and unsupported container expansion
remain explicit in the final Evidence.
Automatic Hopper function matching, process replay, protocols, UI comparison,
and reconstruction verification are not yet stages of this run. Existing
manual comparison Evidence can still be aggregated through the original
`find_changed_behavior` mode.
+13 -22
View File
@@ -1,6 +1,6 @@
# JavaScript Application Graph
JavaScript Application Graph v1 is REA's provider-neutral domain model for
JavaScript Application Graph is REA's provider-neutral domain model for
connecting shipped application bytes, JavaScript structure, Electron process
boundaries, passive browser observations, and native add-ons. It is a durable,
canonical data contract; it is not an extractor or a new CLI/MCP tool.
@@ -32,13 +32,12 @@ adds capture-scoped targets, frames, scripts, workers, and inferred
verification remains a separate later layer. The domain graph itself never
reaches outward to obtain any of those facts.
## Versioned record
## Stored record
A stored graph has this top-level shape:
```text
schema: "JavaScriptApplicationGraph"
schema_version: 1
graph_id: jag_<sha256 of normalized graph semantics>
root_node_ids: jag_node_<sha256>[]
nodes: application entities with one or more observations
@@ -47,14 +46,14 @@ coverage: complete, partial, unknown, or unavailable
limitations: explicit graph-wide constraints
```
Parsing rejects unknown fields, unsupported versions, non-canonical ordering,
duplicate IDs, stale semantic commitments, missing roots, dangling endpoints,
self-edges, and `changed_from` edges between unlike node kinds. Constructors
normalize set-like arrays before deriving their IDs.
Parsing rejects unknown fields, non-canonical ordering, duplicate IDs, stale
semantic commitments, missing roots, dangling endpoints, self-edges, and
`changed_from` edges between unlike node kinds. Constructors normalize set-like
arrays before deriving their IDs.
## Entities and relationships
The v1 node vocabulary covers:
The node vocabulary covers:
- package, installer, and artifact;
- ASAR entry;
@@ -107,7 +106,7 @@ Every observation and edge carries all of the following:
location, or an unavailable reason;
- extractor name, version, operation, and optional executable digest;
- coverage status, truncation, omitted count, and named limits;
- limitations and links to existing Evidence v2 records;
- limitations and links to existing Evidence records;
- a content-derived identifier and its declared stability strategy.
Authority and epistemic state are separate fields. An AST observation does not
@@ -158,16 +157,12 @@ stability. Edge IDs use the same derivation with `relationship-exact`
stability. Node, observation, edge, and graph IDs are recomputed during parsing,
so callers cannot preserve a stale ID after changing semantic content.
## Bounds
## Structure
The v1 boundary admits at most 1,000 roots, 100,000 nodes, 200,000 edges, and 64
observations per node. Each properties object is valid bounded JSON with at most
64 keys, depth 6, 512 structural nodes, and 4,096 characters per string. Arrays
of evidence links, limits, and limitations have independent caps.
These are admission limits, not completeness claims. When an extractor reaches
a limit, it must record partial coverage and the exact named limit. It must not
return a bounded prefix labeled complete.
The graph does not impose aggregate caps on roots, nodes, edges, observations,
or JSON properties. Properties must be valid JSON; field-specific validation
still applies to paths, identifiers, and evidence values. Extractors report
actual omissions in coverage instead of silently dropping a prefix.
## Domain API
@@ -184,7 +179,3 @@ The public domain functions are:
- `serializeJavaScriptApplicationGraph` for canonical JSON;
- `computeJavaScriptApplicationGraphSha256` for a byte-stable verified graph
digest.
Schema evolution is explicit. A future incompatible shape receives a new
`schema_version`; v1 parsing fails with an unsupported-version diagnostic rather
than guessing how to reinterpret it.
+45 -37
View File
@@ -1,35 +1,34 @@
# Cross-layer JavaScript application workflows
REA derives a bounded feature trace from one authenticated JavaScript
REA derives a complete reachable feature trace from one authenticated JavaScript
Application Graph and compares two authenticated graph versions. The MCP tools
are `trace_application_feature`, `trace_javascript_semantics`,
`compare_application_versions`, `compare_source_to_bundle`, and
`compare_javascript_export_shapes`; their CLI equivalents use the same names
with hyphens.
Both workflows consume Evidence v2 produced by
Both 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
observations, relationship inferences, and unknown or unavailable facts retain
their original graph authority.
Current `analyze_javascript_application` Evidence uses result schema v2. It
retains the structural JavaScript Application Graph and a separate semantic
relation graph bound to the same root artifact digest and structural graph ID.
Legacy v1 Evidence remains valid for structural workflows, but semantic tracing
returns an actionable request to reanalyze it rather than treating missing
semantic data as an empty graph.
`analyze_javascript_application` Evidence retains the structural JavaScript
Application Graph and a separate semantic relation graph bound to the same root
artifact digest and structural graph ID. Semantic tracing requires the semantic
relation graph and reports when it is unavailable rather than treating missing
data as an empty graph.
## Feature tracing
Select one literal seed kind: node ID, route, string, API, IPC channel, module,
or native export. Matching is exact or literal substring matching; regular
expressions and executable predicates are not accepted. Direction, depth,
nodes, edges, paths, and seed matches all have caller-visible limits.
expressions and executable predicates are not accepted. The trace includes all
matching seeds and the complete graph reachable in the selected direction.
The result contains the matching basis, a bounded subgraph, terminal paths,
authority summaries, truncation frontier, and native handoffs. A handoff binds
The result contains the matching basis, complete reachable subgraph, terminal
paths, authority summaries, and native handoffs. A handoff binds
the exact native artifact digest and requested exports from the application
graph. Existing Hopper or Ghidra Evidence is linked only when its subject digest
matches exactly. Otherwise the result recommends provider-neutral follow-up
@@ -42,16 +41,15 @@ provider implicitly.
rereading mutable files. A seed may identify a semantic or application node,
literal, function fingerprint, property, endpoint, event, or boundary field.
The query declares backward provenance, forward influence, callers, or
ownership and can restrict admitted relation kinds. Node, relation, depth,
function, module, seed-match, and page limits are validated against exact
published ranges. A committed cursor returns the next deterministic relation
page only for the same graph, query, and limits.
ownership and can restrict admitted relation kinds. REA returns every matching
seed and the complete reachable subgraph; callers do not choose node, relation,
depth, function, module, seed-match, or page budgets.
The extractor covers lexical definitions and reads, literal seeds, static
property slots, object reads/writes/spreads/destructuring, closure captures,
uniquely resolved local calls, argument/return flow, and explicit Promise
construction, chaining, aggregation, await, return, assignment, and
detachment. It also recovers bounded static candidates for EventEmitter
detachment. It also recovers static candidates for EventEmitter
registrations/removals and dispatch, Node timers and cancellation handles, asynchronous
`node:child_process` creation and ownership, configuration sources and direct
defaults, request construction and response consumers, parse/coercion/
@@ -60,9 +58,9 @@ validation boundaries, and built-in resource acquisition/release.
Function fingerprints commit normalized syntax, control-flow shape, relation
shape, literal sets, arity, and detected effects without using local names or
source offsets. Equal duplicate fingerprints remain ambiguous. Dynamic
calls/properties/names, parser recovery, unresolved handles, unsupported API
shapes, and every reached bound stay explicit unknowns; a missing relation is
never reported as proven behavioral absence.
calls/properties/names, parser recovery, unresolved handles, and unsupported API
shapes stay explicit unknowns; a missing relation is never reported as proven
behavioral absence.
Semantic relations use static-inference authority. `resolved` means the static
target identity was unique within admitted analysis; it does not mean the path
@@ -83,19 +81,21 @@ persistent identities. Duplicate candidates remain ambiguous. Source-map and
structural matches are high/medium-confidence inferences rather than exact
facts. Each item is `unchanged`, `added`, `removed`, `changed`, or `unknown`,
and the result includes its basis, candidates, changed dimensions, Evidence
links, limitations, and a bounded `changed_from` graph.
links, limitations, and a `changed_from` graph containing all compared nodes,
their observations, and relationships.
One-sided absence is `added` or `removed` only when the opposite input graph has
complete coverage. With partial, unavailable, or truncated input, the same
condition is `unknown`. Output truncation separately reports omitted comparison
items, candidate references, graph nodes, edges, and observations. With
`unknown_registry_approved: true`, unresolved comparison items can be retained
as a residual unknown in a live session.
condition is `unknown`. The comparison retains every classified item and
ambiguity candidate. Coverage carries omitted counts and limits from each input
graph; comparison adds no caller-selected item, candidate, graph node, or edge
budget. Unresolved comparison items are recorded as a residual unknown linked
to the comparison Evidence in a live session.
## Historical source-to-bundle comparison
`compare_source_to_bundle` compares one cryptographically committed
`HistoricalSourceGraph/v1` with authenticated application-graph Evidence. The
`HistoricalSourceGraph` with authenticated application-graph Evidence. The
stable scoring model reports every admitted signal and weight: exact source
digest, source-map original path, exact current path, path suffix, basename, and
language extension. Exact digest wins over location inference; weak basename
@@ -103,12 +103,13 @@ or extension evidence never forces a mapping.
Each historical source file is classified as `unchanged`, `modified`,
`removed`, `split`, `merged`, `duplicated`, or `unknown`. Multiple equally
scored path mappings form a bounded static split inference. Multiple current
scored path mappings form a static split inference. Multiple current
nodes with the exact historical digest are duplicated; multiple historical
paths with exact identity to one current node are merged. `removed` requires
complete historical and application inventories with no relevant comparison
truncation. Ambiguous candidates, partial inventories, and exhausted limits
remain unknown. These static mappings do not claim runtime loading or semantic
complete historical and application inventories. The comparison analyzes and
returns all source files, candidate nodes, and matching signals represented in
the supplied graphs. Partial inventories remain unknown where absence cannot
be established. These static mappings do not claim runtime loading or semantic
equivalence.
## Export return-shape comparison
@@ -120,9 +121,9 @@ match. An export is linked to a callable only through exact lexical/module
relationships recovered by the AST analysis.
Direct return expressions, including expression-bodied arrows, are evaluated
through a bounded execution-free value lattice. Literal object fields are
static observations. Calls, dynamic spreads, computed keys, parser recovery,
and exhausted value, property, or return-site limits remain partial or unknown.
through an execution-free value lattice. Literal object fields and direct
return sites are represented in the result. Calls, dynamic spreads, computed
keys, and parser recovery remain partial or unknown.
Nested callable returns are not assigned to their parent callable. Projected
graph observations carry source ranges but never source text.
@@ -136,8 +137,9 @@ replay recommendation; it does not execute JavaScript.
## CLI and verification
All five CLI commands accept inline JSON or a JSON file up to 64 MiB. The input
is the same object used by the corresponding MCP tool.
All five CLI commands accept inline JSON or a path to a JSON file. Put the full
Evidence records in the workflow input, as in the corresponding MCP tool; the
CLI does not resolve Evidence IDs from a separate bundle.
For two operator-provided directories or ASARs, run:
@@ -158,8 +160,8 @@ all four `--left-module-path`, `--left-export-name`, `--right-module-path`, and
`--right-export-name` options to verify an explicit export pair.
Because source Evidence and derived Evidence are retained by the normal session
ledger, evidence bundles, analysis snapshots, and investigation workspaces can
carry these records without another persistence format.
ledger, evidence bundles and analysis snapshots can carry these records without
another persistence format.
## Controlled replay boundary
@@ -175,3 +177,9 @@ 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.
+25 -42
View File
@@ -1,12 +1,12 @@
# JavaScript artifact reconstruction
REA can project an operator-supplied Electron/JavaScript application directory
or ASAR into [JavaScript Application Graph v1](javascript-application-graph.md)
or ASAR into [JavaScript Application Graph](javascript-application-graph.md)
without executing application code. The target-free
`analyze_javascript_application` MCP tool, dedicated
`rea analyze-javascript-application` CLI command, and generic
`rea analyze PATH --approved` directory/ASAR route expose the same application
service and return an Evidence v2 envelope containing the graph and an Electron
`rea analyze PATH` directory/ASAR route expose the same application
service and return an Evidence envelope containing the graph and an Electron
boundary summary.
The resulting Evidence can be combined with passive web or Electron capture
@@ -17,50 +17,38 @@ separate static, runtime, and cross-layer inference authorities.
## Public workflow
The administrator first grants a canonical input root. Every request still
requires explicit per-call approval, and the requested ASAR or directory must
remain beneath that root after canonicalization:
The tool reads the caller-selected local directory or ASAR path directly:
```bash
export REA_INVESTIGATION_INPUT_ROOTS_JSON='["/absolute/path/to/apps"]'
rea analyze /absolute/path/to/apps/app.asar --approved --json
rea analyze /absolute/path/to/apps/app.asar --json
rea analyze-javascript-application /absolute/path/to/apps/app.asar \
--approved \
--json
```
Generic `rea analyze` selects this static provider for directories and `.asar`
paths when neither `--provider` nor `--snapshot` is supplied. Use the dedicated
command when setting explicit format or reconstruction limits. Native targets,
single JavaScript files, `.app` bundles, and explicit deep-provider or snapshot
requests retain the native deep-analysis route.
paths when neither `--provider` nor `--snapshot` is supplied. The dedicated
command and the generic route return their complete results inline. Native
targets, single JavaScript files, `.app` bundles, and explicit deep-provider or
snapshot requests retain the native deep-analysis route.
When configuring an MCP client, set the variable before approved setup so REA
copies the explicit non-secret root policy into the managed registration:
Configure an MCP client with the ordinary REA setup command:
```bash
export REA_INVESTIGATION_INPUT_ROOTS_JSON='["/absolute/path/to/apps"]'
rea setup
```
Restart the configured MCP client after changing this administrator ceiling.
An already-running server cannot acquire a new process environment through
MCP elicitation or `SIGHUP`.
The equivalent MCP input is:
```json
{
"input_path": "/absolute/path/to/apps/app.asar",
"format": "auto",
"approved": true,
"source_map_read_approved": false
"format": "auto"
}
```
`input_path` must be absolute. `format` accepts `auto`, `asar`, or `directory`.
The result retains the canonical local path, root artifact digest, artifact
manifest and graph commitments, JavaScript Application Graph v1, static
manifest and graph commitments, JavaScript Application Graph, static
Electron summary, reconstruction statistics, and explicit limitations. It does
not require a live Hopper, Ghidra, browser, or Electron process.
@@ -90,7 +78,7 @@ Selected bounded text is then parsed as inert data to recover:
- static imports, dynamic imports, CommonJS `require` calls, workers, and
service workers;
- route, network endpoint, storage, and vendor-marker observations;
- local source-map declarations and, when separately approved, bounded original
- local source-map declarations and original
source names and content digests;
- explicit BrowserWindow options and `webPreferences`, including statically
resolvable preload entrypoints;
@@ -119,11 +107,7 @@ JavaScript file, package record, or source map produces an `unknown` graph scope
with `state: unavailable`; it is not treated as evidence that modules or source
are absent.
Source-map content has a separate `source_map_read_approved` input. Without that
approval, REA inventories the map, links a static declaration when present, and
records partial non-truncated coverage plus an unavailable parse scope. With
approval, the graph stores original source names and optional content digests,
not the raw `sourcesContent` text.
Source-map contents are parsed as local input. The graph stores original source names and optional content digests, not raw `sourcesContent` text.
Only explicitly present BrowserWindow values are observations. REA does not
substitute version-dependent Electron defaults for omitted `webPreferences`.
@@ -160,30 +144,29 @@ represented by metadata only.
## Bounds and coverage
The input schema bounds artifact entries, cumulative artifact bytes, bytes per
entry, compression ratio, path depth and length, selected text files, text bytes,
AST nodes, static findings, bundle modules, source-map originals, and cooperative
parse time. Source-map original limits are shared across all maps in one
reconstruction. Combined input limits are rejected if their conservative graph
projection could exceed the v1 contract's 100,000 nodes or 200,000 edges.
The reconstruction keeps internal resource safeguards for artifact entries,
cumulative artifact bytes, bytes per entry, compression ratio, path depth and
length, selected text files, text bytes, AST nodes, and cooperative parse time.
These safeguards are not caller-selectable output budgets. Source-map parsing
reports truncation when its format or parser safety boundary is reached. The
application graph has no aggregate node, edge, root, or observation prefix cap.
Byte, entry, path, and graph-shape bounds are hard limits. The parse deadline is
checked before and between bounded parsing and traversal phases; the synchronous
Babel and JSON parser calls cannot be preempted mid-call, so their input byte
bounds remain the hard protection for an individual call.
Byte-identical assets share a content-digest node. At most 64 distinct
observations are retained on that node, as required by the graph contract; every
inventoried containment edge remains present, and any omitted observations make
top-level coverage partial and truncated. When an exact omission count is not
knowable, `omitted_count` is `null` rather than a guessed value.
Byte-identical assets share a content-digest node. Every distinct observation
and inventoried containment edge is retained. If parsing or artifact safety
boundaries omit input, coverage reports that omission; when an exact omission
count is not knowable, `omitted_count` is `null` rather than a guessed value.
## Verification boundary
The source-owned fixture covers an extracted directory, a direct ASAR with an
unpacked native add-on, a direct ASAR whose unpacked native companion is
missing, and an ASAR nested beneath a directory. Tests also cover deterministic
reruns, parse-not-execute behavior, source-map approval, global source-map
reruns, parse-not-execute behavior, source-map parsing, global source-map
limits, malformed structured data, invalid containers, traversal, symlink
escape, oversized text, cancellation, AST truncation, and repeated content
identities. A separate synthetic Electron fixture covers explicit safe and
+19 -24
View File
@@ -1,11 +1,11 @@
# Passive Node and Electron runtime observation
REA can attach to an already-running Node.js or Electron V8 Inspector target
and retain bounded script-load and execution-context metadata. The
and retain script-load and execution-context metadata. The
`list_javascript_runtime_targets` and `observe_javascript_runtime` MCP tools
have equivalent `rea list-javascript-runtime-targets` and
`rea observe-javascript-runtime` commands. Successful calls return
deterministic Evidence v2.
deterministic Evidence.
This authority is disabled by default and is separate from browser CDP,
Electron file-page inspection, Process Capture, and controlled JavaScript
@@ -13,23 +13,19 @@ replay:
```bash
export REA_V8_INSPECTOR_OBSERVE_ENABLED=true
export REA_V8_INSPECTOR_ENDPOINTS_JSON='["http://127.0.0.1:9229"]'
export REA_V8_INSPECTOR_FILE_ROOTS_JSON='["/absolute/path/to/app"]'
export REA_V8_INSPECTOR_ALLOWED_ORIGINS_JSON='[]'
rea list-javascript-runtime-targets http://127.0.0.1:9229 \
--approved --json
--json
rea observe-javascript-runtime http://127.0.0.1:9229 TARGET_ID \
--runtime-kind node --approved --json
--runtime-kind node --json
```
At least one canonical file root or exact HTTP(S) origin is required.
Discovery is restricted to a literal-loopback HTTP endpoint. A target must
match the exact requested target ID, expose a same-port WebSocket reported by
the approved discovery endpoint, and remain inside the requested file or
origin scope. Targets already marked attached are rejected rather than
displacing another debugger. Canonical file paths are checked after symlink resolution.
Excluded locations are counted but never retained.
The caller supplies the literal-loopback HTTP endpoint directly. Selecting an endpoint exposes all targets it serves and their local script locations or renderer origins. A target must match the exact requested target ID and expose a same-port WebSocket reported by that endpoint. Targets already marked attached are rejected rather than displacing another debugger. Local paths are canonicalized after symlink resolution.
`list_javascript_runtime_targets` returns every available target in one inline
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.
## Passive protocol boundary
@@ -73,20 +69,19 @@ Electron main, preload, and renderer behavior can therefore be observed only
as separate approved Inspector targets. The provider does not infer that two
targets belong to the same Electron application.
## Bounds and determinism
## Capture and determinism
Every observation commits the window and exact limits for events, scripts,
execution contexts, per-location bytes, and total retained metadata bytes.
Limit hits increment dropped-event counts and set `capture.truncated`; they
never become completeness claims. Scripts are authorized 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.
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
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.
Inspector attach is inherently incomplete. Enabling `Debugger` reports known
and uncollected scripts, but scripts collected before attachment may be
missing. Absence from a bounded capture never proves that a script or behavior
does not occur.
missing. Absence from the observation window never proves that a script or
behavior does not occur.
## Static correlation
+20 -25
View File
@@ -3,15 +3,15 @@
REA can combine an existing static JavaScript Application Graph with one or
more passive browser, Electron page, or Node/Electron V8 Inspector captures without collapsing their different
authorities. The `reconcile_javascript_runtime` MCP tool and
`rea reconcile-javascript-runtime` CLI command accept Evidence v2 records and
return derived Evidence v2 containing a combined JavaScript Application Graph
`rea reconcile-javascript-runtime` CLI command accept Evidence records and
return derived Evidence containing a combined JavaScript Application Graph
and explicit match, ambiguity, mismatch, and unknown classifications.
The operation performs no filesystem, browser, or Electron I/O. It accepts only
semantically verified Evidence produced by `analyze_javascript_application`,
`inspect_web_page`, `inspect_electron_page`, or
`observe_javascript_runtime`; matching operation names alone
is insufficient. Provider identity, predicate version, subject digest, target
is insufficient. Provider identity, predicate type, subject digest, target
parameters, and normalized-result schemas are checked before reconciliation.
## Workflow
@@ -30,7 +30,7 @@ parameters, and normalized-result schemas are checked before reconciliation.
and asset roots need explicit mappings.
The MCP input has this shape; the `analysis` and `runtime_observations` values
are complete Evidence v2 objects, not only evidence IDs:
are complete Evidence objects, not only evidence IDs:
```json
{
@@ -47,12 +47,7 @@ are complete Evidence v2 objects, not only evidence IDs:
]
}
],
"runtime_observations": ["INSPECT_WEB_PAGE_EVIDENCE"],
"limits": {
"max_runtime_entities": 10000,
"max_reconciliation_items": 20000,
"max_static_load_states": 20000
}
"runtime_observations": ["INSPECT_WEB_PAGE_EVIDENCE"]
}
```
@@ -101,13 +96,13 @@ nodes. REA evaluates compatible static candidates conservatively:
3. Without captured bytes, one unique mapped artifact path can produce a
medium-confidence location match.
4. Multiple candidates remain `ambiguous`; no `observed_as` edge is emitted.
`candidate_static_count` reports the full qualified layer/node count while
`candidate_static_nodes` retains a deterministic bounded prefix.
`candidate_static_count` reports the full qualified layer/node count, and
`candidate_static_nodes` includes every unique candidate reference.
5. If captured bytes disagree with the static candidate at the mapped path,
the result is an explicit mismatch. A path never overrides a digest
disagreement.
6. Missing authorization mappings, incomplete input coverage, and output
limits remain unknown or truncated rather than becoming absence claims.
6. Missing authorization mappings and incomplete or truncated input coverage
remain unknown or truncated rather than becoming absence claims.
Every successful cross-layer `observed_as` edge has
`authority: cross-layer-reconciliation` and `state: inferred`. The underlying
@@ -124,7 +119,7 @@ classification:
- `resident-in-loaded-asset` means a containing asset was observed, but the
embedded module was not independently observed or shown to execute;
- `not-observed-in-capture` means the entity did not appear in the complete
bounded captures supplied to this call and its artifact path falls beneath
authorized captures supplied to this call and its artifact path falls beneath
an exercised automatic or operator-declared mapping prefix;
- `unknown` covers incomplete captures, layers outside runtime scope, and
unresolved correspondence. Nodes outside a mapped artifact prefix stay
@@ -156,18 +151,18 @@ silently equated with the generated bytes observed by CDP.
## Bounds and determinism
Inputs allow up to eight static layers and 32 passive captures. Default output
bounds retain 10,000 runtime entities, 20,000 reconciliation records, and
20,000 static load states; each caller-visible maximum is 50,000. At least one
target node per capture must fit. The combined graph also preserves the JAG v1
caps of 1,000 roots, 100,000 nodes, 200,000 edges, and 64 observations per
node. Reaching a limit reports exact omitted counts for runtime entities,
reconciliation items, static load states, and merged graph items, plus
`coverage.status: truncated`.
The workflow does not impose aggregate count caps on static layers, passive
captures, runtime entities, reconciliation items, static load states, runtime
mapping declarations, or candidate references. These collections remain
subject to the structural validation applied to each Evidence record and
application graph; passive observation returns all in-window collection items
that pass per-value validation. Reconciliation reports zero
omitted items when it projects the complete supplied inputs. Static JavaScript
analysis can still report its own bounded AST coverage as partial or truncated.
Layers, captures, nodes, edges, matches, and evidence links are canonically
ordered. Repeating the same verified Evidence, mappings, and limits produces
the same reconciliation, graph, and Evidence identifiers.
ordered. Repeating the same verified Evidence and mappings produces the same
reconciliation, graph, and Evidence identifiers.
## Non-goals
+29 -30
View File
@@ -4,9 +4,9 @@ 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 bounded metadata, signature, method-body
CIL, exception-region, call-edge, and field-access inspection through
`inspect_managed_members` and `rea inspect-managed-members`, 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
@@ -91,7 +91,7 @@ commitment groups:
- module name, generation, and MVID;
- target framework and runtime/version strings with their exact metadata
locations;
- parser/provider version, profile schema, limits, and profile digest.
- provider identity, analysis-affecting parameters, and their profile digest.
CLI `#GUID` heap values are committed as their exact 16-byte GUID text. REA
does not impose RFC 4122 UUID version or variant bits on MVID, EncId, or
@@ -106,23 +106,22 @@ EncBaseId because ECMA-335 metadata does not require those bit patterns.
- exact raw bytes or bounded value digest;
- raw CIL digest plus separately reported method-header, locals-token, and
exception-region observations where available;
- a deterministic v1 decoded-instruction-tuple digest for completely decoded
- a deterministic decoded-instruction-tuple digest for completely decoded
CIL, with the limitations below.
### Shipped decoded-CIL fingerprint v1
### Shipped decoded-CIL fingerprint
`inspect_managed_members` exposes two method CIL hashes with different byte
boundaries:
| Field | Shipped v1 input | Excluded from the digest |
| Field | Input | Excluded from the digest |
| ---------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `il_sha256` | The exact `il_size` raw CIL bytes after the tiny or fat method header | Method header, locals signature blob, alignment, and extra sections including exception clauses |
| `normalized_il_sha256` | UTF-8 `JSON.stringify` output for the instruction-order array of `[opcode, operand_kind, operand]` tuples | Instruction offsets except projected scalar branch targets, switch targets, resolved metadata/string identities, method header, locals, and exception regions |
The normalized field is available only when the entire CIL stream decodes with
status `present`. A partial, malformed, absent, or over-limit body reports
`null`. The containing member result's `schema_version: 1` scopes the tuple
format; v1 does not expose a separate algorithm-version field.
status `present`. A malformed or absent body reports `null`. The tuple format
is defined here.
Tuple operands use these exact projections:
@@ -134,17 +133,17 @@ Tuple operands use these exact projections:
- signed integer and variable operands: decimal strings; floating-point
operands: JavaScript's `String` representation of the decoded IEEE value.
Opcode names retain short/long forms and prefix opcodes. This makes v1 a
Opcode names retain short/long forms and prefix opcodes. This makes the value a
reproducible decoded-tuple fingerprint, not a canonical semantic CIL identity:
it does not resolve tokens across MVIDs, does not commit full control flow or
exception semantics, and does not replace the raw CIL digest. Cross-build
comparison uses the value only as one unique-only structural tier and reports
ambiguity when that evidence is insufficient. A future normalized-CIL v2 must
have an explicit algorithm version, canonical token/literal identities,
ambiguity when that evidence is insufficient. A future normalized-CIL
commitment would need canonical token/literal identities,
complete branch and switch targets, and explicit locals/exception semantics.
For example, a decoded `nop; ret` body serializes exactly as
`[["nop","none",null],["ret","none",null]]` and has v1 digest
`[["nop","none",null],["ret","none",null]]` and has digest
`5e5fad7741cb44bca3a4f045546b7449990da343f612f5f34c0ca30e9eee0636`.
This vector specifies the tuple order, `null` handling, absence of whitespace,
UTF-8 encoding, and lowercase hexadecimal output.
@@ -170,7 +169,7 @@ that implement them. The underlying capability slices are:
1. **Triage and inventory**: container/component classification, assembly and
module identity, references, files, exported types, resources, attributes,
and runtime markers.
2. **Member inventory**: bounded types, methods, fields, properties, events,
2. **Member inventory**: file-backed types, methods, fields, properties, events,
interfaces, nesting, generics, and normalized signatures.
3. **Method inspection**: headers, max stack, locals, CIL instructions,
constants, metadata operands, exception regions, and implementation flags.
@@ -191,9 +190,11 @@ that implement them. The underlying capability slices are:
Evidence-backed application graph vocabulary as JavaScript/Electron
findings while preserving managed static-analysis authority.
Every list has a deterministic order and hard limits. Search exposes scanned,
matched, returned, and dropped counts. An unresolved indirect call or a failed
signature decode is not silently omitted from a completeness claim.
Inventory lists preserve deterministic order and include every fact available
from the admitted PE/CLI streams. Malformed ranges and unsupported metadata are
reported as coverage issues instead of being silently omitted. Search exposes
scanned, matched, returned, and dropped counts. An unresolved indirect call or
a failed signature decode is not silently omitted from a completeness claim.
## Obfuscation-resistant method slices
@@ -205,7 +206,7 @@ bounded set of observations needed to investigate a proposition:
- exact strings, numeric constants, enum-like values, and serialization keys;
- branch, switch, exception, loop, call, and field-flow shape;
- construction and generic-instantiation sites;
- raw CIL and decoded-tuple-v1 commitments, plus separate header, locals, and
- raw CIL and decoded-tuple commitments, plus separate header, locals, and
exception observations;
- callers, callees, shared fields, and competing candidates; and
- limitations from reflection, dynamic dispatch, generated code, protection,
@@ -218,12 +219,11 @@ turn that role into the method's durable identity.
Cross-version matching is two-stage:
1. Exact identity requires compatible artifact/module commitments and exact
raw CIL identity; v1 does not expose a standalone complete-method-body
digest.
2. Structural identity compares schema-versioned signatures, the limited v1
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 v1 digest does not itself remap
evidence does not distinguish them. The digest does not itself remap
metadata tokens.
Tokens are always remapped through observed structure. A caller cannot carry
@@ -327,7 +327,6 @@ absolute or relative to the manifest file. A compact manifest contains:
```json
{
"schema_version": 1,
"label": "operator-local osu!stable semantic slice",
"target": {
"path": "./osu!.exe",
@@ -373,10 +372,10 @@ Before evaluating methods, the verifier fails closed on target SHA-256, MVID,
assembly name, runtime-family, and managed-architecture mismatches whenever the
manifest supplies those fields. It then pages each declared MethodDef token
directly by row, so selected methods do not need to appear in the first member
page of a large application. `il_length` remains accepted as a legacy alias for
`il_size`; optional `il_sha256` locks the exact raw CIL bytes in addition to
REA's member-result-v1 decoded-instruction-tuple digest. That normalized field
has the v1 limits documented above and is not a complete semantic CIL identity.
page of a large application. Optional `il_sha256` locks the exact raw CIL bytes
in addition to REA's decoded-instruction-tuple digest. That normalized field
has the byte boundaries documented above and is not a complete semantic CIL
identity.
The optional `application_graph` block reuses those exact-build method
commitments. For each referenced MethodDef token, the verifier builds a bounded
@@ -424,8 +423,8 @@ The managed-code track advances as reviewable pull requests:
1. accepted evidence/provider boundary (this document and ADR);
2. read-only artifact triage and exact identity (shipped);
3. bounded metadata, signatures, method bodies, raw CIL hashes, and the limited
decoded-instruction-tuple fingerprint v1 (shipped; normalized-CIL v2
3. file-backed metadata, signatures, method bodies, raw CIL hashes, and the
decoded-instruction-tuple fingerprint (shipped; complete normalized-CIL
semantics remain planned);
4. obfuscation-resistant slices and cross-version comparison (shipped for
static member observations);
+33 -56
View File
@@ -2,14 +2,11 @@
## Identity and discovery
`rea://server/identity` and `binary_session` report the package, server, SDK,
negotiated protocol, skill, and schema-sensitive catalog identities separately.
An absent live comparison is `unknown`, never aligned. Callers can supply the
expected package version, catalog digest, or registered server path to
`binary_session`; a mismatch means the registered MCP process must be restarted.
`rea doctor` separately inspects supported JSON/TOML client registrations,
reports their command vectors as aligned, stale, missing, or invalid, and keeps
live-server state `unknown` unless the active connection supplies identity.
`binary_session` reports the active package, server, SDK, and negotiated
protocol details. `rea doctor` separately inspects supported JSON/TOML client
registrations, reports their command vectors as aligned, stale, missing, or
invalid, and keeps live-server state `unknown` unless the active connection
supplies identity.
Canonical tool names remain stable, while `tools/list` advertises only the
operations callable for the current target, provider, policy, host, and
@@ -48,7 +45,7 @@ REA accepts ordinary `tools/call` progress tokens. Updates are monotonic,
rate-bounded to at most one intermediate update per 100 ms, and always allow a
terminal update. Unknown totals are omitted; REA does not fabricate percentages.
Provider calls receive the request cancellation signal. Artifact traversal,
hashing, cross-version scanning/checkpoints, Hopper requests, and process capture
hashing, version comparisons, Hopper requests, and process capture
check the same signal. Cancellation is distinct from timeout. A cleanup failure
uses `cleanup_incomplete` and lists only the owned resource kinds that remain.
Derived comparisons and reconstruction verification yield before computation
@@ -58,36 +55,24 @@ CLI calls work without a progress token and translate SIGINT into the same
AbortSignal used by providers. Existing controlled-process cleanup and provider
shutdown rules still apply; REA never kills a process it cannot prove it owns.
## Evidence resources
## Tool results
Evidence remains inline for compatibility and successful Evidence-producing
tools also return `resource_link` content. Session resources are connection-local:
Evidence-producing tools return their result and Evidence context inline. Read
that result directly. Use `get_evidence_bundle` when the task needs broader
retained session history or an explicit bundle for transfer.
- `rea://evidence/{evidenceId}` returns the complete immutable Evidence v2 record.
- `rea://evidence/{evidenceId}/section/{section}` returns a bounded result section.
Stable sections include `result`, `terminal`, `filesystem`, `process`,
`protocol`, `nodes`, `occurrences`, and `edges` when present.
- `rea://unknown/{unknownId}` returns the current residual-unknown head and its
immutable revision history.
- `rea://unknowns/active` returns current unresolved heads.
- `rea://snapshot/{snapshotDigest}` returns the current immutable analysis
snapshot when its canonical content digest matches.
- `rea://artifact/{manifestId}/{collection}` returns a canonical artifact
`nodes`, `occurrences`, or `edges` page with Evidence provenance.
- `rea://function/{targetSha256}/{address}` returns a retained function dossier
for the exact target and address.
- `rea://workspace/{workspaceId}/revision/{revision}` returns an immutable,
CAS-linked investigation workspace revision retained by this session.
## Aggregate native context
Successful automatic cross-version investigations include both Evidence and
workspace `resource_link` blocks. Workspace resources preserve revision and
`previous_revision_digest` commitments; persistent workspace files remain
subject to configured read/write roots.
An Evidence URI is discoverability, not authorization. It cannot authorize file
access, extraction, mounting, execution, or networking. IDs disappear when the
session ends unless the existing Evidence bundle or workspace persistence flow
explicitly retains them.
`get_navigation_context` composes the selected document, current address, and
current/containing procedure. Its capability inventory exposes a
`current_selection` mode and an `explicit_document` mode; the latter works when
the caller supplies `document` and the provider lacks `current_document`. A
cursor outside a procedure is represented as
`procedure: null`. `inspect_address_context` requires an explicit address and
returns name, procedure, comment, inline-comment, and bookmark facets;
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
@@ -100,14 +85,15 @@ Existing environment settings map to administrator ceilings. They remain the
maximum authority:
- process roots, executables, environment names, and external networking;
- Evidence read/write roots;
- investigation input and workspace roots;
- snapshot read/write roots;
- reference-source roots;
- native mount enablement.
Artifact extraction retains its existing explicit per-call approval and maps to
an administrator root ceiling of `/` for compatibility.
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.
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.
`rea policy status`, `list`, `explain`, and `revoke` inspect the same evaluator
used by MCP. Optional project grants require both
@@ -137,11 +123,7 @@ 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` uses the `investigation_input` capability. Its
absolute `input_path` must be inside `REA_INVESTIGATION_INPUT_ROOTS_JSON` and the
request must set `approved: true` before any artifact read. Reading source-map
contents additionally requires `source_map_read_approved: true`; ordinary input
approval does not imply that separate authority.
`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.
## Integrity record-and-continue
@@ -152,12 +134,7 @@ all three conditions:
2. `integrity_policy=record-and-continue`;
3. explicit per-call `integrity_continue_approved=true`.
Contradictory bytes are quarantined from nested expansion, recorded with declared
and observed hashes, trust, provenance, path, and unpacked state, and bounded by
`max_integrity_mismatches`. Verified siblings continue. Comparisons classify the
result as a contradiction and reconstruction cannot treat it as unchanged.
`investigate-versions` accepts the same policy through
`--integrity-policy record-and-continue --integrity-continue-approved` and
`--max-integrity-mismatches`. Its Evidence pages and CAS-linked workspace retain
contradictions, so a completed run can resume without rescanning or weakening
trust labels.
Contradictory bytes are quarantined from nested expansion and recorded with
declared and observed hashes, trust, provenance, path, and unpacked state.
Verified siblings continue. Comparisons classify the result as a contradiction
and reconstruction cannot treat it as unchanged.
+39 -37
View File
@@ -6,19 +6,28 @@ invoke tools by themselves.
## Prompt inventory
| Prompt | Purpose |
| --------------------------------- | ------------------------------------------------------------------------------------- |
| `investigate_feature` | Trace a feature from discovery into bounded function evidence. |
| `compare_application_versions` | Compare two shipped artifacts, with optional static and runtime follow-up. |
| `verify_reconstruction` | Evaluate a finite reconstruction specification against retained comparison Evidence. |
| `trace_crash` | Correlate a crash symptom with static paths and optional Process Capture v4 evidence. |
| `audit_residual_unknowns` | Audit current residual-unknown heads and evidence-qualified resolution. |
| `prepare_bounded_process_capture` | Design an approval-gated, bounded process experiment before execution. |
| Prompt | Purpose |
| --------------------------------- | ------------------------------------------------------------------------------------ |
| `investigate_feature` | Trace a feature from discovery into relevant function evidence. |
| `compare_application_versions` | Compare two shipped artifacts, with optional static and runtime follow-up. |
| `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. |
Every rendered prompt provides an ordered list of current REA tool names. It
also requires the agent to keep observations, inferences, and unknowns
distinct. Requested prompt arguments and completion choices are rendered as
untrusted selection data, not instructions or authorization.
Every rendered prompt provides optional starting points using current REA tool
names. Agents can call tools directly, skip irrelevant suggestions, and inspect
provider or target state only when it helps answer the request or a tool
requires it. The prompts also ask agents to keep observations, inferences, and
unknowns distinct. Requested prompt arguments and completion choices are
rendered as untrusted selection data, not instructions or authorization.
Tool results include their Evidence inline. Prompts do not require a bundle or
resource fetch to read a result; an Evidence ID is only a reference when a
prompt argument or tool explicitly accepts one.
For target paths in `compare_application_versions`, the prompt suggests
opening each target before calling `inspect_artifact`; inspection operates on
the active target and returns the graph and findings together inline.
Use standard MCP discovery and retrieval:
@@ -45,11 +54,10 @@ cached prompt catalog.
## Session-aware completion
MCP `completion/complete` currently completes prompt arguments and resource
template variables. It does not define completion for arbitrary tool-call
arguments. REA therefore attaches completion to optional guided-prompt
arguments that feed later tool selection; it does not claim protocol-level
completion for every REA tool.
MCP `completion/complete` completes guided-prompt arguments; it does not define
completion for arbitrary tool-call arguments. REA attaches completion to
optional prompt arguments that accept a session value. Suggestions provide
identifiers only; they do not retrieve or replace the inline Evidence result.
```json
{
@@ -74,28 +82,22 @@ completion for every REA tool.
Completion sources are live projections of the current session:
| Argument family | Source and filtering |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Document | Current provider `list_documents` result. |
| Procedure | Bounded pagination over `list_procedures`; observed addresses are offered from partial results, while names require exhaustive discovery and a unique address. Optional document context is forwarded. |
| Provider | `auto` plus sorted deep candidates from the current binary session; target-free completion does not start them. |
| Evidence | Evidence IDs retained in the current session ledger. |
| Process capture | Evidence IDs whose operation and validated result identify Process Capture v4. |
| Artifact manifest | Manifest IDs from schema-valid retained artifact inventories. |
| Artifact occurrence | Occurrence IDs present in schema-valid retained artifact inventory pages. |
| Residual unknown | Current non-resolved unknown heads only. |
| Argument family | Source and filtering |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Document | Current provider `list_documents` result. |
| Procedure | The complete `list_procedures` inventory; all procedure addresses and uniquely named procedures are offered. Optional document context is forwarded. |
| Provider | `auto` plus sorted deep candidates from the current binary session; target-free completion does not start them. |
| Evidence | Evidence IDs retained in the current session ledger, offered only for prompt arguments that accept Evidence references. |
| Process capture | IDs for retained Evidence whose operation and validated result identify process capture. |
| Artifact manifest | Manifest IDs from schema-valid retained artifact inventories. |
| Artifact occurrence | Occurrence IDs present in schema-valid retained artifact inventory pages. |
| Residual unknown | Current non-resolved unknown heads only. |
Suggestions are Unicode-normalized, case-insensitive prefix matches. The server
deduplicates them, rejects values longer than 4,096 characters, and sorts by
code point for deterministic results. It scans at most 10,000 candidates;
procedure discovery is further bounded to 5,000 unique addresses in pages of 500. If that bound is reached before exhaustive procedure discovery, only
observed addresses are suggested because global name uniqueness is unknown.
The MCP result carries at most 100 `values`. `total` is the number of matching
candidates found within REA's scan bound, and `hasMore` reports whether more
than 100 were found. The MCP completion response has no offset or cursor, so a
client narrows a large result by sending a longer prefix rather than requesting
another page.
deduplicates them and sorts by code point for deterministic results. Procedure
completion derives candidates from the complete inventory, so a name is offered
only when it identifies one address in that inventory. Completion returns all
matching values; it does not expose paging controls.
## Lifecycle and safety
+146
View File
@@ -0,0 +1,146 @@
# Native UI and dispatch investigation
REA joins static resources, native metadata and code facts while keeping runtime
observations separate. Every result reports its evidence, target identity,
coverage and unknowns. The CLI and MCP use the same application workflows.
## Static inspection
- `inspect_asset_catalog` / `rea inspect-asset-catalog <app>` reads compiled
`Assets.car` metadata through macOS `assetutil --info`. Catalog digests, raw
rendition fields, pagination and exact UI resource-name matches are returned.
Image extraction and undocumented rendition interpretation are unsupported.
- `inspect_keyed_archive` / `rea inspect-keyed-archive <plist>` reads XML or
binary Foundation keyed archives without instantiating classes. For an active
app, provide its relative archive path as the second CLI argument or MCP
`path`. Results retain original `$objects` indices, named `$top` roots,
shared/cyclic references, class descriptors, raw serialized fields and
malformed/unresolved/nil references. Missing fields stay absent; unknown
classes are preserved. Select `root`, `offset` and `limit` explicitly.
UID 0 cannot distinguish conditional nil from ordinary nil. Limits are
64 MiB of input, 200,000 objects/references and 128 nesting levels.
- `decode_interface_builder` / `rea decode-interface-builder <app>` reads
compiled storyboard and nib resources into objects, outlets, actions,
connections and controller/class names. `verify:interface-builder` compiles
a source-owned AppKit XIB with `ibtool`. Storyboards require an installed
iOS platform; unsupported archive forms remain explicit.
- `inspect_native_dispatch_metadata` / `rea inspect-native-dispatch-metadata
<app-or-binary>` prefers a validated macOS Mach-O byte reader. It decodes
64-bit little-endian Objective-C class/metaclass records, superclass pointers,
absolute/relative method entries, ivar offsets/sizes/alignment and protocol
declarations. It also decodes simple Swift conformances, static synchronous
witness slots, signed-relative pointers and non-generic, non-resilient class
vtable descriptors. Records include exact virtual addresses/file offsets and
artifact evidence. Vtable indexes are metadata word offsets; witness indexes
start after the conformance header. Names unavailable in metadata remain null.
Chained fixups, external bindings, authenticated pointers, categories,
properties, generic/resilient/async/coroutine tables and inherited overrides
have explicit unsupported or partial coverage. Other providers retain the
existing symbol-based inventory with its narrower coverage.
## Native instruction, call and type primitives
Ghidra supplies three exact-object operations:
```bash
rea inspect-native-instruction <binary> <address> --provider ghidra
rea resolve-native-call-targets <binary> <call-site> --provider ghidra
rea inspect-native-data-type <binary> --type /MyStruct --provider ghidra
rea inspect-native-data-type <binary> --address <typed-data-address> --provider ghidra
```
Instruction facts include bytes, length, decoder text, ordered register/scalar/
address tokens, typed references and flow destinations. Mid-instruction, data,
outside-memory and undecodable addresses have separate outcomes. Effective
memory base/index/displacement roles and per-instruction context mode remain
unavailable; `mode` is the program language variant.
Call resolution reports direct, resolved indirect, ambiguous, unresolved and
non-call outcomes from static call references. It does not establish runtime
execution or classify Objective-C/Swift/vtable/closure mechanisms from names.
Type inspection selects one exact database pathname or typed data address.
Struct/union fields, enums, pointers, arrays, typedefs, size, alignment, packing
and bitfields retain database authority. Child types use exact IDs for further
inspection, including recursive layouts. Source/debug authority and flexible
array semantics are not inferred. The real lane uses a native DWARF 4 object
with a struct containing a union, enum and recursive pointer; a stripped linked
binary may have no corresponding recovered layout.
## Bounded static traces
`trace_native_ui_action` / `rea trace-native-ui-action <target> <seed>` accepts
one authored selector/object ID, native symbol or exact function address. It
joins UI wiring to uniquely matched class/selector implementations and follows
bounded typed call references. Direct references, resolved indirect references,
ambiguous candidates, inferred untyped provider callees and targetless call
sites remain distinct. A static route does not establish runtime reachability.
Swift/closure dispatch requires resolved static references; unavailable ABI
forms remain unknown.
`trace_native_values` / `rea trace-native-values <binary> <procedure> --provider
ghidra` composes high-p-code def-use graphs across statically resolved calls.
Logical CALL arguments bind to recovered parameter ordinals; recovered RETURN
values bind to CALL outputs. These edges are decompiler-derived. Constants,
operators, LOAD/STORE and branch operands are included inline. Alias effects,
persistent state, RNG roles, missing/variadic bindings and ambiguous destinations
remain unknown. Budgets control depth, decompilations, call-site resolutions,
nodes and edges. Node payloads are bounded to 8 MiB and serialized results to
32 MiB. Pagination returns edges whose source is on the node page and preserves
stable IDs for endpoints outside that page.
The raw Java `is_dead` flag can remain set after Ghidra decodes a live block;
`block_membership` reports actual syntax-tree membership separately.
The provider dossier itself retains up to 3,000 p-code operations, 64 inputs per
operation and 12,000 def-use edges, with explicit omitted counts. A `STORE`
may describe stack memory and does not itself prove persistent state.
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
`observe_native_ui` captures one explicit existing PID/window ID after
`observation_approved: true`. 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/
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.
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.
## 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.
- `npm run verify:ghidra`: host-native debug/stripped targets, native type layout,
instruction/call facts, value dependencies and process/project cleanup.
- `npm run verify:ghidra:aarch64-jump-table`: optimized ELF and byte/halfword
relative tables, plus ARM64 Mach-O on an ARM64 macOS host.
- `npm run verify:apple-dispatch`: source-built Objective-C protocols/classes and
Swift conformances/vtables, repeated after stripping local symbols.
- `npm run verify:native-ui`: one source-owned fixture window, selected capture
or a reported OS permission denial, changed-target rejection and cleanup.
macOS ARM64 is the real host verified during this implementation. Admission of
macOS Intel does not claim an Intel verification run. Unsupported metadata and
unresolved runtime/value semantics remain visible in results.
+38 -23
View File
@@ -1,4 +1,4 @@
# Process Capture v4
# Process capture
Process Capture records one approved command as deterministic Evidence. It is
intended for authority-versus-reconstruction checks where terminal behavior,
@@ -11,7 +11,7 @@ HTTP/WebSocket exchanges, and process exit ownership. Missing or bounded
observations remain explicit; a truncated capture is never treated as
equivalent to another capture.
Every v4 capture includes a run manifest with canonical SHA-256 commitments for
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
@@ -47,18 +47,19 @@ 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. The MCP SDK runtime currently pinned by REA negotiates the older
protocol path, so the interactive flow remains fail-closed until its runtime
protocol support is available.
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.
Every scenario must also contain `"approved": true`. Executables, working
directories, filesystem roots, and requested environment variables are checked
against operator policy before launch. Process Capture is an observation tool,
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 v4
`rea capture-process` reads a JSON scenario and writes process capture
Evidence to stdout:
```bash
@@ -69,7 +70,6 @@ A representative scenario is:
```json
{
"approved": true,
"executable": "/absolute/path/to/node",
"arguments": ["./signup.mjs"],
"working_directory": "/absolute/project",
@@ -115,13 +115,20 @@ 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.
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.
## Reactive process scenarios
Set `reactive` to a Process Reactive Scenario v1 declaration when interaction
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, bounded uses and visits, trigger predicates, ordered actions, and
either a next state or `passed` outcome.
transitions, caller-chosen use and visit counts, trigger predicates, ordered
actions, and either a next state or `passed` outcome.
The admitted runtime slice matches decoded UTF-8 terminal literals and exact
`terminal_raw`, `interaction`, `process`, `filesystem`, `http`, `websocket`, or
@@ -129,7 +136,7 @@ The admitted runtime slice matches decoded UTF-8 terminal literals and exact
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
bounded `all`, `any`, `sequence`, and `repeat` nodes and may select a frontier at
`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`
@@ -138,9 +145,12 @@ target launches.
Reactive results are recorded in `reactive_run`, including the terminal outcome,
active state, matched event IDs, action evidence IDs, and deterministic
transition journal. 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,
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
@@ -156,9 +166,10 @@ 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 bounded by
the scenario protocol-event limit; exceeding it marks the whole capture
truncated while replay continues.
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
@@ -171,9 +182,13 @@ 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 boundary accepts at most 10,000 events, 4 MiB of declared action
JSON, 1,024 action text fields, and 1,024 possible sensitive-capture operations.
These caps bound both returned content and secret-redaction work.
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
+73 -202
View File
@@ -1,22 +1,21 @@
{
"catalog_schema_version": 1,
"package": {
"name": "rea-agents",
"version": "2.5.1",
"version": "3.2.1",
"sdk": {
"server": "2.0.0-beta.4",
"client": "2.0.0-beta.4",
"core": "2.0.0-beta.4"
"server": "2.0.0",
"client": "2.0.0",
"core": "2.0.0"
},
"skill_version": "23"
},
"tools": {
"total": 110,
"total": 122,
"families": [
{
"id": "direct",
"surface": "official-proxy",
"count": 36,
"count": 39,
"tools": [
"address_name",
"address_to_file_offset",
@@ -26,6 +25,8 @@
"current_procedure",
"goto_address",
"inline_comment",
"inspect_native_data_type",
"inspect_native_instruction",
"list_bookmarks",
"list_documents",
"list_names",
@@ -44,6 +45,7 @@
"read_bytes",
"read_function_instructions",
"resolve_containing_procedure",
"resolve_native_call_targets",
"search_procedures",
"search_strings",
"set_address_name",
@@ -59,7 +61,7 @@
{
"id": "enhanced",
"surface": "enhanced",
"count": 12,
"count": 15,
"tools": [
"analyze_function",
"analyze_swift_types",
@@ -70,28 +72,39 @@
"get_call_graph",
"get_objc_classes",
"get_objc_protocols",
"swift_classes",
"inspect_native_api",
"inspect_native_dispatch_metadata",
"trace_call_path",
"trace_feature"
"trace_feature",
"trace_native_ui_action",
"trace_native_values"
]
},
{
"id": "native",
"surface": "native-provider",
"count": 5,
"count": 7,
"tools": [
"capture_native_ui_scenario",
"demangle_swift",
"inspect_macho",
"inspect_plist",
"inspect_signature",
"list_architectures"
"list_architectures",
"observe_native_ui"
]
},
{
"id": "artifact",
"surface": "artifact-provider",
"count": 3,
"tools": ["extract_artifact", "inspect_artifact", "inventory_artifact"]
"count": 5,
"tools": [
"decode_interface_builder",
"extract_artifact",
"inspect_artifact",
"inspect_asset_catalog",
"inspect_keyed_archive"
]
},
{
"id": "managed",
@@ -127,9 +140,10 @@
{
"id": "electron",
"surface": "electron-provider",
"count": 4,
"count": 5,
"tools": [
"analyze_javascript_application",
"capture_electron_scenario",
"inspect_electron_page",
"list_electron_targets",
"reconcile_javascript_runtime"
@@ -147,17 +161,15 @@
{
"id": "application",
"surface": "application-workflow",
"count": 12,
"count": 10,
"tools": [
"build_reconstruction_obligation_ledger",
"commit_reconstruction_coverage",
"compare_application_versions",
"compare_javascript_export_shapes",
"compare_source_to_bundle",
"evaluate_reconstruction_readiness",
"evaluate_reconstruction_coverage",
"execute_node_characterization",
"prepare_node_characterization",
"query_reconstruction_coverage",
"run_controlled_replay",
"trace_application_feature",
"trace_javascript_semantics"
@@ -166,7 +178,7 @@
{
"id": "session",
"surface": "session",
"count": 19,
"count": 22,
"tools": [
"binary_session",
"build_call_path",
@@ -179,7 +191,10 @@
"correlate_static_and_runtime",
"export_evidence_bundle",
"find_changed_behavior",
"get_evidence_bundle",
"get_navigation_context",
"import_evidence_bundle",
"inspect_address_context",
"list_unknowns",
"open_binary",
"record_unknown",
@@ -199,6 +214,8 @@
"capabilities": [
"address_name",
"analyze_function",
"inspect_native_data_type",
"inspect_native_instruction",
"list_documents",
"list_names",
"list_procedures",
@@ -213,6 +230,7 @@
"procedure_references",
"read_function_instructions",
"resolve_containing_procedure",
"resolve_native_call_targets",
"search_procedures",
"search_strings",
"xrefs"
@@ -267,11 +285,13 @@
"name": "macOS native inspection utilities",
"version": null,
"capabilities": [
"capture_native_ui_scenario",
"demangle_swift",
"inspect_macho",
"inspect_plist",
"inspect_signature",
"list_architectures"
"list_architectures",
"observe_native_ui"
]
},
{
@@ -279,9 +299,11 @@
"name": "REA safe artifact graph provider",
"version": "1",
"capabilities": [
"decode_interface_builder",
"extract_artifact",
"inspect_artifact",
"inventory_artifact"
"inspect_asset_catalog",
"inspect_keyed_archive"
]
},
{
@@ -339,14 +361,12 @@
"version": "1",
"capabilities": [
"build_reconstruction_obligation_ledger",
"commit_reconstruction_coverage",
"compare_application_versions",
"compare_javascript_export_shapes",
"compare_source_to_bundle",
"evaluate_reconstruction_readiness",
"evaluate_reconstruction_coverage",
"execute_node_characterization",
"prepare_node_characterization",
"query_reconstruction_coverage",
"run_controlled_replay",
"trace_application_feature",
"trace_javascript_semantics"
@@ -364,6 +384,12 @@
"version": "1",
"capabilities": ["capture_browser_scenario"]
},
{
"id": "rea-playwright-electron-active",
"name": "REA Playwright active Electron observation provider",
"version": "1",
"capabilities": ["capture_electron_scenario"]
},
{
"id": "rea-v8-inspector",
"name": "REA passive Node/Electron V8 Inspector provider",
@@ -418,170 +444,8 @@
"configuration": "detect-only"
}
],
"schemas": [
{
"id": "analysis_snapshot",
"version": 2
},
{
"id": "artifact_extraction",
"version": 1
},
{
"id": "artifact_graph",
"version": 1
},
{
"id": "browser_scenario",
"version": 1
},
{
"id": "browser_scenario_capture",
"version": 1
},
{
"id": "browser_scenario_diff",
"version": 1
},
{
"id": "browser_target_list",
"version": 1
},
{
"id": "electron_page_inspection",
"version": 1
},
{
"id": "electron_target_list",
"version": 1
},
{
"id": "evidence",
"version": 2
},
{
"id": "evidence_bundle",
"version": 2
},
{
"id": "evidence_completion_ledger",
"version": 1
},
{
"id": "evidence_completion_manifest",
"version": 1
},
{
"id": "investigation_run",
"version": 1
},
{
"id": "investigation_run_summary",
"version": 1
},
{
"id": "investigation_workspace",
"version": 1
},
{
"id": "javascript_application_analysis",
"version": 2
},
{
"id": "javascript_application_graph",
"version": 1
},
{
"id": "javascript_application_version_comparison",
"version": 1
},
{
"id": "javascript_feature_trace",
"version": 1
},
{
"id": "javascript_runtime_observation",
"version": 1
},
{
"id": "javascript_runtime_target_list",
"version": 1
},
{
"id": "javascript_semantic_graph",
"version": 1
},
{
"id": "javascript_semantic_query",
"version": 1
},
{
"id": "javascript_semantic_trace",
"version": 1
},
{
"id": "managed_artifact_inspection",
"version": 1
},
{
"id": "managed_member_comparison",
"version": 1
},
{
"id": "managed_member_inspection",
"version": 1
},
{
"id": "managed_native_boundary_inspection",
"version": 1
},
{
"id": "managed_native_verification",
"version": 1
},
{
"id": "process_capture",
"version": 4
},
{
"id": "reconstruction_verification",
"version": 1
},
{
"id": "residual_unknown",
"version": 1
},
{
"id": "web_bundle_analysis",
"version": 1
},
{
"id": "web_capture_diff",
"version": 1
},
{
"id": "web_mcp_discovery",
"version": 1
},
{
"id": "web_observation_session",
"version": 1
},
{
"id": "web_page_inspection",
"version": 2
},
{
"id": "web_screenshot",
"version": 1
},
{
"id": "web_screenshot_diff",
"version": 1
}
],
"cli": {
"primary_count": 66,
"primary_count": 75,
"commands": [
"analyze",
"analyze-javascript-application",
@@ -589,9 +453,10 @@
"build-reconstruction-obligation-ledger",
"capabilities",
"capture-browser-scenario",
"capture-electron-scenario",
"capture-native-ui-scenario",
"capture-process",
"capture-web-screenshot",
"commit-reconstruction-coverage",
"compare",
"compare-application-versions",
"compare-javascript-export-shapes",
@@ -600,11 +465,12 @@
"compare-source-to-bundle",
"compare-web-captures",
"compare-web-screenshots",
"decode-interface-builder",
"decompile",
"demangle-swift",
"discover-webmcp-tools",
"doctor",
"evaluate-reconstruction-readiness",
"evaluate-reconstruction-coverage",
"evidence-export",
"evidence-import",
"execute-node-characterization",
@@ -614,30 +480,35 @@
"import-reference-source",
"inspect",
"inspect-artifact",
"inspect-asset-catalog",
"inspect-electron-page",
"inspect-keyed-archive",
"inspect-macho",
"inspect-managed-artifact",
"inspect-managed-members",
"inspect-managed-native-boundaries",
"inspect-native-api",
"inspect-native-data-type",
"inspect-native-dispatch-metadata",
"inspect-native-instruction",
"inspect-plist",
"inspect-signature",
"inspect-web-page",
"instructions",
"inventory-artifact",
"investigate-versions",
"list-architectures",
"list-browser-targets",
"list-electron-targets",
"list-javascript-runtime-targets",
"observe-javascript-runtime",
"observe-native-ui",
"observe-web-session",
"plan-managed-runtime-correlation",
"policy",
"prepare-node-characterization",
"project-managed-application-graph",
"providers",
"query-reconstruction-coverage",
"reconcile-javascript-runtime",
"resolve-native-call-targets",
"run-controlled-replay",
"run-replay-machine",
"search",
@@ -645,6 +516,8 @@
"trace",
"trace-application-feature",
"trace-javascript-semantics",
"trace-native-ui-action",
"trace-native-values",
"uninstall",
"upgrade",
"verify-managed-native-boundaries",
@@ -659,17 +532,15 @@
},
"runtime_catalog": {
"counts": {
"cli_commands": 66,
"mcp_tools": 110,
"mcp_prompts": 6,
"mcp_resources": 2,
"mcp_resource_templates": 11
"cli_commands": 75,
"mcp_tools": 122,
"mcp_prompts": 6
},
"digests": {
"tools_sha256": "9a033ddd40fcb8273365cc895bbf75d943bf1c5c8c2ee1eb9c4b3494064aaf58",
"prompts_sha256": "f6da3ad38a1000c11364d1710f122ac4990551f04efd8c53ffb2e819b5cf5707",
"resources_sha256": "339a984ea8cd8be5f234adf24395bf0295258d990492005cf6fe1f293d12cd46",
"combined_sha256": "e8cac51f2f17a5b9a088c3ff773473c4897823401481fc0875a28fe7daf8eee0"
"tools_sha256": "1a32398486b1e65f50a1a81de2d8867e6a4c296c165bf9994e53034b466c41b4",
"prompts_sha256": "fa889fc671185ac9c968c6dc8309cf609e709cdc837dbbf39b7cdfadb84ac2d7",
"combined_sha256": "a87e48bc626cb4361f494c074e71852304bfedee17fbba89976d32bb6244f230",
"providers_sha256": "c5a88a49990fafc8244972ee8ff543fc19995eb79db960da55f6a311e15340d1"
}
}
}
+59 -52
View File
@@ -1,20 +1,20 @@
# Static-analysis provider evaluation
Status: the Ghidra read-only analysis provider is shipped on Linux x64 and has
an experimental Windows x64 P0 for approved native PE applications. It
validates an exact bring-your-own Ghidra 12.1.2/JDK 21 environment, resolves a
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 19 operation-level capabilities after the authenticated
import, and publishes 22 operation-level capabilities after the authenticated
post-analysis handshake.
The provider-neutral target, provider registry, deterministic target binding,
analysis-profile commitment, Evidence provenance, snapshot v2, and bounded
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, bounded search, function metadata,
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`,
@@ -34,14 +34,14 @@ follow.
- Start with bring-your-own Ghidra and a compatible Java runtime. Setup must not
install or upgrade Java.
- Run Ghidra headlessly in an owned process with a private temporary project,
bounded startup/analysis/request deadlines, cancellation, and cleanup.
startup and cleanup deadlines, caller cancellation, and complete replies.
- Prefer a packaged Java bridge loaded through Ghidra's script path. PyGhidra
remains useful for prototypes but is not a mandatory production dependency.
- Treat authenticated `ping` and `shutdown` as lifecycle proof only. Inventory,
xref, CFG, and decompilation claims require separately admitted operation
contracts and real-provider conformance.
- Implement read-only inventories, assembly, decompilation, function metadata,
calls, references, containment, and bounded search first.
calls, references, containment, and complete inventory search first.
- Report GUI cursor/navigation and persistent mutation operations as unavailable
until their semantics and project ownership are explicitly designed.
- Verify real claims on Linux with at least two distinct source-owned binaries.
@@ -51,7 +51,7 @@ follow.
## Shipped foundation boundary
`GHIDRA_INSTALL_DIR` must identify an extracted official 12.1.2 release;
`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
@@ -61,9 +61,9 @@ other formats, and other architectures before launch.
The launcher creates one ephemeral runtime root with project,
home/cache/config/data/temp, logs, descriptor, endpoint, target snapshot, and
ownership manifest.
It passes `-readOnly`, `-deleteProject`, a 300-second per-file analysis limit,
two CPUs, and a 2 GiB heap; inherited Java option injection variables are
cleared. On Linux, the mode-0600 descriptor carries the random token without
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
@@ -83,43 +83,50 @@ capability from a successful import.
## Admitted inventory semantics
| Concern | Ghidra contract |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Program identity | One `analyzeHeadless` import produces exactly one Program; `list_documents` therefore returns exactly one name. |
| Addresses | Default memory uses lowercase `0x` hexadecimal. Non-default and external spaces use `<percent-encoded-space>:0x<hex>` and remain round-trippable. The handshake commits image base and default address-space name. |
| Symbols | `list_names` includes address-bearing memory and external symbols, including dynamic symbols, while excluding variable and no-address namespace records. Each item reports primary, dynamic, external, symbol type, and source facts. |
| Procedures | Both non-external and external functions are listed. A local thunk remains distinct from its resolved target; exact and qualified name lookup fails on ambiguity rather than guessing. |
| Strings | Only Ghidra-defined string `Data` is observed. Items report charset, byte length, and whether a required null terminator is missing. The API cannot distinguish a present terminator from fixed/Pascal layouts when no terminator is missing, so that state is named `present_or_not_required`. |
| Memory | Memory-block end addresses are exclusive. Read/write/execute, initialization, overlay, address space, and image base are direct Ghidra observations. |
| Pagination | List limits are 500 and search limits are 100. Pages commit exact totals and advancing offsets; an inventory above one million items fails rather than returning a partial result labeled exhaustive. |
| Search | Literal search scans the immutable inventory with a 1,000,000-unit cumulative work budget. Regex uses a conservative finite Java-regex subset with 10,000 static paths, 4,096 UTF-16 code units per candidate, and the same cumulative budget. Exceeding a budget fails explicitly; returned values identify truncation. |
| Analysis state | The socket is exposed only after default auto-analysis. A 300-second analysis timeout fails target opening; ordinary established requests have a 10-second client deadline and every response has a 1 MiB ceiling. |
| Concern | Ghidra contract |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Program identity | One `analyzeHeadless` import produces exactly one Program; `list_documents` therefore returns exactly one name. |
| Addresses | Default memory uses lowercase `0x` hexadecimal. Non-default and external spaces use `<percent-encoded-space>:0x<hex>` and remain round-trippable. The handshake commits image base and default address-space name. |
| Symbols | `list_names` includes address-bearing memory and external symbols, including dynamic symbols, while excluding variable and no-address namespace records. Each item reports primary, dynamic, external, symbol type, and source facts. |
| Procedures | Both non-external and external functions are listed. A local thunk remains distinct from its resolved target; exact and qualified name lookup fails on ambiguity rather than guessing. |
| Strings | Only Ghidra-defined string `Data` is observed. Items report charset, byte length, and whether a required null terminator is missing. The API cannot distinguish a present terminator from fixed/Pascal layouts when no terminator is missing, so that state is named `present_or_not_required`. |
| Memory | Memory-block end addresses are exclusive. Read/write/execute, initialization, overlay, address space, and image base are direct Ghidra observations. |
| Inventory | Procedure, symbol, and string listings plus searches return the complete matching collection in one response; callers do not provide offsets or result-count limits. |
| Search | Literal and Java-regex searches scan the complete immutable inventory and return all matching entries inline. Search waits for a reply or caller cancellation; there are no caller-supplied offsets or result-count limits. |
| Analysis state | The socket is exposed only after default auto-analysis completes. Established operations wait for their reply or caller cancellation; there is no fixed response-size ceiling. |
## Admitted function-analysis semantics
| Concern | Ghidra contract |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Decompiler lifetime | One persistent `DecompInterface` is opened for the imported Program and disposed during bridge shutdown. Each native decompile has a 30-second deadline; the socket leaves a bounded 35-second projection window. External functions or functions without bodies return `null`; timeout, cancellation, and native failure remain distinct. |
| Serialization | A bounded FIFO admits at most 32 active-plus-queued requests and sends one Program request at a time. Queue wait counts against the caller's deadline, and queued cancellation is prompt. This is an adapter safety commitment, not a claim that every Ghidra API is thread-safe. |
| Function identity | Every function result carries the entry address and Ghidra FunctionManager classification for external, thunk, and resolved thunk target. These are observations, not proof that unresolved targetless calls have been recovered. |
| Assembly and pseudocode | Assembly is bounded Ghidra Listing text; pseudocode is bounded Ghidra decompiler output. Neither is original source, and cross-provider comparison never treats Hopper and Ghidra text as equal or unequal semantic facts. |
| Instruction fast path | `read_function_instructions` returns one offset-paginated raw Listing window without invoking the decompiler or whole-program name/string inventories. It reports scan work, exact-or-unknown totals, truncation, and continuation metadata. |
| Calls and references | Callers/callees contain only resolved functions. Reference edges preserve exact ReferenceManager type and call/jump/data/read/write/indirect/computed/conditional/terminal/external facts. Targetless computed flow remains unknown. Synthetic entry-point references without actionable memory sources are omitted explicitly. |
| CFG | Dossiers use `BasicBlockModel` and retain only non-call successors inside the function body. CFG topology is address-normalized for comparison; provider-specific block construction remains a declared difference. |
| Bounds | Direct assembly fails above 100,000 instructions or the wire ceiling. Dossiers scan at most 5,000 requested instructions, return bounded independent pages, mark unknown totals after an incomplete scan, and paginate pseudocode by Unicode code points. |
| Concern | Ghidra contract |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Decompiler lifetime | One persistent `DecompInterface` is opened for the imported Program and disposed during bridge shutdown. Native decompilation has no fixed per-function deadline. External functions or functions without bodies return `null`; cancellation and native failure remain distinct. |
| Serialization | A FIFO sends one Program request at a time. Caller cancellation removes queued work promptly and is passed to active socket waits. This is an adapter safety commitment, not a claim that every Ghidra API is thread-safe. |
| Function identity | Every function result carries the entry address and Ghidra FunctionManager classification for external, thunk, and resolved thunk target. These are observations, not proof that unresolved targetless calls have been recovered. |
| Assembly and pseudocode | Assembly is complete Ghidra Listing text; pseudocode is Ghidra decompiler output. Neither is original source, and cross-provider comparison never treats Hopper and Ghidra text as equal or unequal semantic facts. |
| Instruction fast path | `read_function_instructions` returns every raw Listing instruction for the requested function without invoking the decompiler or whole-program name/string inventories. |
| Calls and references | Callers/callees contain only resolved functions. Reference edges preserve exact ReferenceManager type and call/jump/data/read/write/indirect/computed/conditional/terminal/external facts. Targetless computed flow remains unknown. Synthetic entry-point references without actionable memory sources are omitted explicitly. |
| CFG | Dossiers use `BasicBlockModel` and retain only non-call successors inside the function body. CFG topology is address-normalized for comparison; provider-specific block construction remains a declared difference. |
| P-code value flow | Function dossiers include up to 3,000 high-p-code operations, at most 64 inputs per operation, and 12,000 def-use edges. Results identify memory reads/writes, branches, and calls by p-code opcode, and mark decompiler-dead operations. Large results report truncation, an omitted-operation lower bound, and known omitted-input/edge counts. The dossier is an intra-function graph; `trace_native_values` composes derived parameter/argument and return/output dependencies across resolved calls. It does not resolve memory aliasing, call side effects, runtime behavior, persistent state, or business meaning. |
| Result extent | Function instruction scans and native API boundary observations remain complete. P-code flow is the explicit bounded exception; its count fields distinguish exact retained counts from lower-bound or known-omitted counts. Caller cancellation and provider failures remain distinct. |
`npm run verify:ghidra` compiles the versioned C oracles into x86-64 debug and
stripped ELF, AArch64 ELF, x86-64 PE, and x86-64 Mach-O targets. It proves all
19 operations, external functions, resolved thunks, exports, stripped-name
`npm run verify:ghidra` compiles the versioned C oracle into debug and stripped
host-native targets (x86-64 ELF on Linux x64 or Mach-O on macOS), plus a native
DWARF 4 type-layout object. It proves the admitted operations, external functions, resolved thunks, exports, stripped-name
behavior, direct and targetless indirect calls, typed references, strings/xrefs,
multi-block CFG, semantic enhanced workflows, cancellation, deadlines,
multi-block CFG, bounded p-code def-use/effect links, semantic enhanced workflows, cancellation, startup deadlines,
serialized concurrency, malformed-target rejection, profile identity, and
process/project cleanup against real Ghidra 12.1.2. Unit fixtures separately
cover analysis/decompile timeouts, process exit, queue saturation, and malformed
wire output.
process/project cleanup against real Ghidra 12.1.4. Unit fixtures separately
cover startup deadlines, process exit, queued cancellation, and malformed wire
output.
`npm run verify:ghidra:cross-format` adds AArch64 ELF, x86-64 PE, and x86-64
Mach-O target coverage. It requires `clang`, LLD, and `lld-link`; set
`REA_CLANG`, `REA_LLD`, or `REA_LLD_LINK` to select them. The verifier checks
these tools before compilation. Keeping this matrix separate lets Linux
host/provider acceptance run with only the host compiler.
`npm run verify:ghidra:windows` generates a deterministic source-owned native
x86-64 PE application, proves all 19 operations through the production Windows
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
@@ -130,7 +137,7 @@ gates.
`src/process/` now provides the mechanisms that a long-lived Hopper or Ghidra
adapter genuinely shares: POSIX run-token-authenticated process-group ownership,
ephemeral temporary runtime roots, one absolute startup deadline, correlated
request timeout/cancellation cleanup, bounded stdout and stderr retention with
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
@@ -145,12 +152,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, bounded process and queue, packaged bridge, 19 admitted 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 | 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. |
## Recommended order
@@ -170,7 +177,7 @@ Before implementation, an adapter proposal must provide:
- a capability matrix covering supported, unsupported, and degraded results;
- provider identity, version, analysis profile, target digest, authority,
limitations, and deterministic locations in every Evidence v2 record;
limitations, and deterministic locations in every Evidence record;
- bounded subprocess or library lifetime, cancellation, timeouts, and cleanup;
- actionable local diagnostics that retain paths, digests, mismatch locations,
and provider metadata while redacting credentials, authorization headers,
@@ -184,9 +191,9 @@ prematurely claiming equivalence or redistribution support.
## Primary-source notes
- Ghidra 12.1.2 documents headless batch mode and its Java runtime requirements
in its [release-specific Getting Started guide](https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.2_build/GhidraDocs/GettingStarted.md);
the [official release](https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.2_build)
- Ghidra 12.1.4 documents headless batch mode and its Java runtime requirements
in its [release-specific Getting Started guide](https://github.com/NationalSecurityAgency/ghidra/blob/Ghidra_12.1.4_build/GhidraDocs/GettingStarted.md);
the [official release](https://github.com/NationalSecurityAgency/ghidra/releases/tag/Ghidra_12.1.4_build)
supplies the corresponding distribution and checksums.
- Rizin lists supported formats, architectures, tools, and `rzpipe` bridges in
its [repository README](https://github.com/rizinorg/rizin); `rz-pipe` documents
+7 -15
View File
@@ -1,13 +1,11 @@
# Reconstruction obligation ledgers
`build_reconstruction_obligation_ledger` turns authenticated Evidence v2 records
into a versioned, deterministic list of reconstruction claims. The same workflow
`build_reconstruction_obligation_ledger` turns authenticated Evidence records
into a deterministic list of reconstruction claims. The same workflow
is available as:
- MCP tool: `build_reconstruction_obligation_ledger`
- CLI command: `rea build-reconstruction-obligation-ledger`
- MCP resource:
`rea://evidence/{evidenceId}/reconstruction-obligation-ledger`
The ledger is conservative by design. Static Application Graph facts create
candidate obligations; they do not prove runtime or process behavior. A required
@@ -29,7 +27,6 @@ never claims closure:
rea build-reconstruction-obligation-ledger \
'{
"evidence_bundle": {
"bundle_version": 2,
"artifacts": [],
"providers": [],
"environments": [],
@@ -40,18 +37,15 @@ rea build-reconstruction-obligation-ledger \
},
"reviewed_obligations": [],
"manifest": {
"schema_version": 1,
"bindings": [],
"contradictions": []
},
"limits": { "max_obligations": 100 },
"page": { "offset": 0, "limit": 50 }
}
}' \
--json
```
For a real investigation, export an Evidence bundle from the session and replace
the empty bundle. Application Graph and Process Capture v4 records generate
the empty bundle. Application Graph and process capture records generate
conservative candidates automatically. Add reviewed obligations when a required
claim is not represented by those sources.
@@ -326,8 +320,6 @@ close a `packaged-process` obligation. A static-only source cannot close a
runtime or process claim. Marking a claim `blocked` or `out-of-scope` records the
disposition but does not silently count it as verified.
Pagination changes only `page` and the returned `obligations` slice. Every page
retains the same `ledger_id`, `closure_digest`, full-ledger summaries, reports,
ownership graph, dependency graph, Evidence links, and limitations. Consumers
should follow `next_offset` until it is `null`, and should reject pages whose
identity or closure digest changes mid-read.
The tool returns every obligation inline. Its `ledger_id` and `closure_digest`
identify the complete result, including summaries, reports, ownership and
dependency graphs, Evidence links, and limitations.
+11 -17
View File
@@ -1,7 +1,7 @@
# Reconstruction readiness conformance
Run the complete source-owned native, JavaScript CLI, Electron, Evidence,
comparison, obligation, replay, CLI, and MCP journey from a clean checkout:
Run the source-owned native, JavaScript CLI, Electron, Evidence, comparison,
obligation, replay, and cleanup conformance verifier from a clean checkout:
```sh
npm ci
@@ -10,10 +10,10 @@ npm run verify:readiness
The command builds the redistributable native conformance fixtures, inventories
the native executable through the public CLI, analyzes the generated JavaScript
CLI and Electron fixture through both CLI and MCP, verifies exact advertised
limits, evaluates the same nine-stage report through both surfaces, reads the
full MCP report resource, and proves stale/tampered replay rejection. It prints
one compact JSON summary.
CLI and Electron fixtures through both CLI and MCP, evaluates an internal
nine-stage conformance report, and proves stale/tampered replay rejection. The
report evaluator is part of the repository verifier, not a public REA tool. It
prints one compact JSON summary.
Set `REA_READINESS_REPORT_PATH` to retain the complete machine-readable report:
@@ -24,17 +24,11 @@ REA_READINESS_REPORT_PATH=readiness-report.json npm run verify:readiness
CI runs this command on Linux, publishes the compact summary, and retains the
full report as the `reconstruction-readiness` artifact.
## Public contracts
## Project conformance contract
- CLI: `rea evaluate-reconstruction-readiness <json-or-file> --json`
- MCP tool: `evaluate_reconstruction_readiness`
- Full MCP resource:
`rea://evidence/{evidenceId}/reconstruction-readiness-report`
The MCP tool result contains only report identity, status, summary, metrics, and
the resource URI. The full Evidence bundle, stage records, comparisons,
contradictions, obligation ledger, and replay inputs stay in the retained
resource instead of consuming the tool-response context.
The verifier runs as `npm run verify:readiness`; it is not exposed as a public
CLI command or MCP tool. The synthetic report input and evaluator exist to
check project-owned conformance fixtures and fail-closed report behavior.
## Required stages
@@ -51,7 +45,7 @@ resource instead of consuming the tool-response context.
| `export-replay` | CLI/MCP parity, process/structured failure parity, deterministic replay, tamper rejection, and stale-input rejection |
Every stage and required check must occur exactly once and link to Evidence in
the embedded Evidence v2 bundle. `fail`, `unknown`, `unsupported`, `truncated`,
the embedded Evidence bundle. `fail`, `unknown`, `unsupported`, `truncated`,
and `skipped` never aggregate to `pass`.
## Fail-closed rules
+8 -3
View File
@@ -9,9 +9,10 @@ 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.
The Ghidra foundation supports Linux x64 with exact Ghidra 12.1.2 and a 64-bit
full JDK 21. 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/macOS setup propagates them
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
@@ -19,6 +20,10 @@ 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.
+169
View File
@@ -0,0 +1,169 @@
# Test suite audit
## Scope and priority
The expanded audit on 2026-10-04 reviewed every one of the 355 Vitest files
present at its start: 82 domain/contracts, 56 application/composition, 147
adapter/configuration/process/filesystem/CLI files, and 70 MCP/acceptance/
conformance/evaluation files. Three review passes checked the proposed removals
against existing stronger coverage and checked the final changes for lost
assertions. The remaining suite has 351 files.
Prefer full real-provider E2E, then integration across schemas and transports,
then real-data goldens. Remove lower-level duplication once the replacement
asserts the same behavior. Keep distinct failure regressions that a successful
E2E cannot exercise. Do not lower coverage thresholds to accommodate pruning.
## Changes made
| Previous test or assertion | Decision | Replacement or retained claim |
| ----------------------------------------------------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Asset application tests using fabricated `.car` bytes and injected utility JSON | Remove two plumbing cases | `verify:asset-catalog`: real actool → assetutil → CLI and stdio MCP, including digest, full metadata, pagination and invalid input |
| Keyed archive positive binary/XML objects assembled with the same plist library as the reader | Remove two synthetic success cases | Captured Foundation XML plus reviewed complete object/reference graph golden; real Foundation binary/XML → CLI and stdio MCP |
| Generic contract schema/annotation/example loops and output-map inventory comparisons | Consolidate into one SDK wire test | Assert exported schemas, guidance, effect annotations and valid examples on the actual advertised MCP catalog |
| Ghidra function operation enum membership | Remove | Canonical catalog, provider handshake and actual operation roundtrips establish admission; malformed provider results remain covered |
| Asset page assertion pinning `10_001` | Remove | Invalid-page rejection through real MCP; malformed utility data still covered |
| Suggested prompt tool positions | Remove incidental ordering assertions | Keep authority, evidence, untrusted input and preparation constraints |
| Package “acceptance” assertions pinning CI matrix/shard values and fake launcher command arrays | Remove configuration-shaped cases | Actual package/install verifier remains; retain a compact release verification-before-publish and registry-canary static guard |
| Package failure classifier under acceptance | Move to package evaluation | Preserve all malformed/unrelated-failure rejection cases; do not call them E2E |
| Native value graph checks conditional on success | Strengthen | Require success before pagination/ambiguity assertions; unexpected errors can no longer pass |
| Native UI verifier treating permission denial as a positive pass | Separate outcomes | Default lane requires real capture/action success; opt-in permissions lane explicitly reports no positive E2E proof |
Ten previous test cases were removed or consolidated; three replacement
Vitest cases cover the captured goldens and release guard. The two real artifact
E2E scripts add product-level evidence outside the deterministic Vitest count.
## Exhaustive follow-up
The follow-up removes or consolidates another 31 test declarations, including
three redundant test files. Parameterized declarations and executed cases have
different counts; the figures above count source declarations. It also repairs
assertions that could pass without exercising their stated behavior.
| Area | Cleanup | Stronger retained evidence |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Configuration | Merge duplicate target/loader tests and preserve custom launcher, explicit provider and actionable error diagnostics | Independent malformed inputs; each capability flag is rejected separately |
| Browser scenarios | Remove fake launch success and expression-selected artifact success | Real Chromium scenarios, actual artifacts, CLI capture and owned/external cleanup in `verify:browser` |
| CLI policy | Remove duplicate confirmation helper cases | Compiled CLI proves project files stay unchanged until `--yes`; distinct interactive choices remain |
| Provider capabilities | Remove numerical catalog freezes; consolidate Hopper byte-read discovery | Exact canonical capability identities, admission, mutation effects and detached descriptors |
| Evidence ledger | Merge reordered-key checks into serialized bundle import; remove one-record quota claims | Atomic import, conflicts, residual revisions, actual large collections and byte regressions |
| MCP batching and concurrency | Replace maximum concurrency and empty indistinguishable responses | Every distinct ordered batch result; concurrently gated requests complete in reverse order with matching payloads |
| MCP inventories and graphs | Remove traversal/inventory call order and redundant catalog/prose checks | Exact complete cyclic graph, full inline inventories, canonical SDK catalog and all-handler roundtrips |
| Authority and lifecycle | Attempt the refused Ghidra mutation; require successful setup, call and switch; close twice | Typed refusal with zero mutation dispatch, restored target calls and exact owned-resource cleanup |
| Process and provider output | Remove incidental environment read and command option order/defaults | PID/ownership validation, exact disabled-policy failure and complete decoded response above 10 MiB |
| Conformance and CI guards | Merge identical replay runs; remove duplicate missing-row fault, trivial hash/self equality and incidental worker/step counts | Distinct faulty evidence, digest order, actual test discovery, file existence and release/runner authority |
| Pure schemas | Remove fabricated comparison-dimension injection and impossible long boundary success | Real artifact comparison and SDK coverage evaluation with returned boundary identity |
The removed long boundary selector test only parsed a 1,000-character request;
workspace boundary identities admit at most 200 characters, so that positive
could never represent a successful evaluation. The actual MCP evaluation stays
covered. One unused ledger serialization helper was also retired after removing
its only test consumer; no CLI or MCP contract changed.
No further immediately safe pruning was identified in the final review. Retained
replacement candidates below are coverage gaps, rather than permission to delete
unique tests. Test-only domain scaffolding also stays pending a separate decision
about the corresponding source behavior; deleting those tests alone would leave
unverified code and fail unused-export checks.
## Stronger replacements from the closing pass
Three more synthetic success cases were removed. Native value composition now
runs through the production CLI and a separate stdio MCP subprocess with real
Ghidra, using a source-built native fixture. It checks argument/parameter/return
relationships, the fixture's global operand, authenticated Evidence, upstream
and workflow analysis profiles, identical CLI/MCP graphs, discovery availability
and successful MCP session closure. Ambiguity, cancellation, target changes,
missing flow and bounded-work regressions remain in the focused suite.
That replacement found missing CLI workflow routing for `trace_native_values`
and missing workflow profile classification. The routing and discovery now use
the composed operation's actual provider prerequisites; workflow Evidence commits
to the upstream Ghidra profile.
The lone hand-built client-document equality test is replaced by a real TOML
filesystem transaction: reversed environment keys leave the original bytes
unchanged and create no backup. A real ZIP entry with an application root longer
than 5,000 characters replaces injected schema-only positive data. Inventory
completeness tests partition actual scans into authenticated partial observations
and require exact assembled results and every source citation, including more
than 100 citations. These are consumer merge tests, not provider pagination
claims. Replay aggregation now covers all four statuses and retains actual runner
diagnostics; it does not claim to execute a real sandbox. Retained pixel-metric
fixtures now write correct PNG chunk checksums.
The all-zero screenshot buffer is replaced by a real Chromium PNG of 9,467,940
bytes, transported intact through CLI and stdio MCP and decoded by the production
pixel comparator. The verifier configures the SDK client's receive buffer because
its 10 MiB default is smaller than the inline JSON response. This does not claim
large comparison-request transport coverage. A real SDK initialize request,
captured during a successful production stdio handshake, replaces the JSON-RPC
decoder's field-count-only oracle with exact field/value expectations.
The closing pass also removed assertions tied to internal call order, defaults,
private cache sizes, duplicated polling and fabricated schema extensions. Unique
malformed-input, security-capacity and cleanup regressions remain.
## Real process regression found
The full suite exposed a renderer ordering race. Publishing a terminal output
journal entry could synchronously trigger a reactive resize before the preceding
output was queued for rendering. Queue that output first. The existing real
source-owned interactive PTY test now also requires the first resized frame to
contain the output that triggered the resize, preserving the original event
order rather than sorting or changing timestamps to conceal it.
## Coverage kept
Malformed archives, broken and cyclic references, unsupported metadata pointers,
relative Objective-C method layouts, ambiguous call candidates, digest mismatch,
cancellation, permission approval, bounded output and owned-resource cleanup
remain covered. These cases provide evidence beyond a successful real-tool run.
The hand-assembled relative method fixture stays until compiler-produced
relative-list fixtures establish equivalent assertions.
## Remaining priorities
1. Extend the real Ghidra CLI/stdio MCP journey from value tracing to dispatch
and UI handlers before pruning their hand-authored composition success cases.
Native value tracing now establishes full transport parity.
2. Real binary dispatch and nib → full product transports, with normalized
semantic goldens. The compiler-backed readers already run in the new Apple
CI lane; synthetic pointer/layout rejection tests remain useful.
3. An approved interactive macOS runner for native UI capture/actions, including
stdio MCP scenarios. Hosted runners without permissions cannot supply this
proof. No arbitrary application's windows should be opened for this lane.
4. Real macOS/Linux Ghidra CI runners with bring-your-own installations. This
change adds Apple artifact CI without installing Java or Ghidra.
5. Classify fake-runner replay/application acceptance accurately and replace
repeated happy paths only after real sandbox journeys assert the same claims.
The replay workflow's allow-unavailable mode must not imply executed replay.
6. Replace repeated-array schema ceiling tests with real producer/transport
regressions before deleting them. Existing integration tests do not yet
establish their large-input guarantees.
Some static guards protect irreversible release authority rather than product
behavior. Keep these few guards explicit and separate from E2E evidence.
## Verification after the expanded audit
- `npm run check:pr`: 351 files passed; 1,794 tests passed and two skipped.
Typecheck, lint, formatting, unused-code and generated-document gates passed.
- `npm run check:fast`: passed; 70 lint warnings and zero errors.
- `npm run verify:browser`: actual Chromium observation and scenario CLI routes,
retained artifacts, owned/external lifecycle checks and large CLI/stdio MCP PNG
capture passed.
- `npm run verify:ghidra`: macOS ARM64 with Ghidra 12.1.4/JDK 21 passed, including
real native value tracing through CLI and stdio MCP: 171 nodes, 298 edges and
six decompilations.
- `npm run verify:asset-catalog`: real actool/assetutil → CLI and stdio MCP,
complete records, pagination and malformed input checks passed.
- `npm run verify:keyed-archive`: real Foundation binary/XML → CLI and stdio MCP,
full graph golden, shared/cyclic identity and malformed scope checks passed.
- `npm run verify:fixtures`: seven source-built host fixtures, source/artifact
digests and exhaustive inventory checks passed.
No Hopper windows were opened for this audit. The closing pass runs real Ghidra
on macOS ARM64; earlier Hopper evidence remains separate. No new hosted CI success
is claimed.
+208
View File
@@ -0,0 +1,208 @@
# Testing REA
Prefer evidence in this order: full end-to-end workflows with real production
providers, integration tests across data/API boundaries, then golden regressions
from real captured inputs. Keep focused module tests for distinct failure or
semantic cases that these workflows cannot reliably reproduce. A test's path
or suite name does not establish its behavioral depth.
Tests are grouped into Vitest projects so ownership, allowed dependencies and
runtime cost are visible from their paths. Before pruning a test, identify the
replacement scenario and its assertions; passing success journeys do not
replace malformed input, cancellation, permission or cleanup coverage.
## Behavioral depths
| Depth | Location | What it proves |
| ------------ | ----------------------- | ----------------------------------------------------------------------------------------------------- |
| Module | `src/**/*.test.ts` | One domain, service, or adapter through its narrow public surface |
| Composition | `tests/composition/**` | Provider-neutral session and registry wiring; filesystem access is limited to fixture materialization |
| Boundary | `tests/boundary/**` | Exactly one production filesystem, process, network, browser, CLI, or provider boundary |
| MCP boundary | `tests/boundary/mcp/**` | MCP transport and tool-contract boundaries with isolated sessions and explicit cleanup |
| Acceptance | `tests/acceptance/**` | A compiled CLI or MCP journey; injected providers still make it integration |
| Conformance | `tests/conformance/**` | Shared provider contracts, parameterized by declared capabilities and explicit opt-outs |
| Evaluation | `tests/evaluation/**` | Deterministic evaluator parsing, scoring, and report generation |
Colocated domain tests may import only their owning domain and inward
dependencies. They do not construct sessions or servers and do not start
subprocesses or network listeners. Colocated application tests exercise one
service through explicit ports and recording adapters, never a CLI or MCP
entrypoint. Composition tests may assemble provider-neutral sessions and
registries but do not cross production filesystem, process, socket, or browser boundaries.
Boundary tests cross one production boundary. Only acceptance tests assemble
the complete runtime or invoke the compiled product surface.
Focused immutable builders and recording ports shared by one test family live
beside their production owner as `src/**/*.fixture.ts`. They are typechecked
with the suite and excluded from package builds; broader runtime and provider
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,
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.
## End-to-end, integration and golden evidence
Full E2E tests invoke the production command dispatcher and real providers,
without fake launchers, runners or responses. `verify:keyed-archive` writes an
actual Foundation binary archive, runs the CLI and a separate stdio MCP
subprocess, checks parity and pagination, rejects an escaping path and checks
an XML graph golden. `verify:asset-catalog` compiles source-owned colors with
`actool`, invokes real `assetutil`, then checks CLI/MCP results, exact catalog
digest, every raw metadata field, pagination and malformed input rejection.
Neither artifact workflow requires Hopper or launches it. Both run in macOS CI.
MCP SDK transport tests with recording providers remain integration tests.
They are useful for schema drift and failure projection but do not prove that
Hopper, Ghidra or another substituted engine works. `verify:package` proves
packaging/install behavior and fake-provider integration; use the corresponding
real-provider lanes for engine claims. Real Apple dispatch and Interface
Builder verifiers currently prove format integration through production readers.
Golden tests use immutable captured text inputs with producer/source provenance
under `tests/fixtures/golden/`. Expected results are reviewed for the semantic
claim; capture commands do not automatically approve new expected outputs.
Do not call handcrafted utility output or synthetic binary builders real-data
goldens. Keep unsupported binary layouts and malformed boundaries as targeted
regressions until a real fixture establishes equivalent coverage.
`verify:browser` also captures a source-owned noise canvas as a real PNG above
8 MiB through the CLI and stdio MCP, with complete byte/digest parity and real PNG
decoding. Its SDK client explicitly permits the larger inline JSON response;
this lane does not establish large image-comparison request transport coverage.
The [test suite audit](test-suite-audit.md) records the pruning decisions,
replacement evidence and remaining priorities.
## Real-toolchain verification lanes
Each real-toolchain command must require only the host tools needed to prove
its stated claim. Use a host-native fixture for host/provider acceptance, and
place optional cross-target formats or platform-specific runners in separate
commands. Check prerequisites before starting expensive work and name the
missing command, target, and lane in any failure message. A lane must not imply
that a host or target is covered when it was skipped.
| 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: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 |
The host-native Ghidra lane also verifies native value tracing through the
production CLI and a separate stdio MCP process. It compares complete dependency
graphs, validates Evidence and upstream/workflow profiles, checks capability
discovery, and closes the MCP session. No provider or transport is mocked.
The 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.
## Apple Interface Builder archives
`npm run verify:interface-builder` compiles the source-owned AppKit XIB into a
real `.nib` with Xcode `ibtool`, wraps it in a temporary app bundle, and checks
the decoded view hierarchy, outlet, action, evidence coverage, and truncation
status. Storyboard compilation additionally requires an installed iOS platform.
Keep the provider-specific acceptance path independent from optional
cross-compilers. Cross-format failures belong to the cross-format lane and must
not make native host acceptance unavailable.
## Developer commands
`npm test` runs every deterministic project once. The narrower feedback loops
are:
| 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 |
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.
Local full-suite Vitest runs use up to two workers and schedule projects one
at a time. Process, acceptance and process-global projects serialize their
files to prevent competing lifecycle observations. CI retains its two-worker
budget.
The pure domain/contracts and recording-port service projects share one worker
module context because their tests own no mutable runtime resources. MCP
boundary files also share the immutable server module graph while creating and
closing independent in-memory sessions. Adapter, composition, acceptance,
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.
Vitest and Node persistent compile caches are deliberately not enabled by
default. To evaluate repeated local runs, opt in for both cold and warm
measurements with an isolated cache:
```bash
NODE_COMPILE_CACHE=.cache/node-compile npm test
```
Do not report the warm result as a cold-suite improvement, and do not enable
the cache in coverage or benchmark CI without first showing that its
instrumentation remains equivalent.
## Coverage and timing
CI owns coverage. The aggregate floors are 65% statements, 60% branches, 60%
functions, and 68% lines. `src/domain/**` must reach 80% statements, 75%
branches, 75% functions, and 80% lines. `src/contracts/**` must reach 85%
statements, 80% branches, 80% functions, and 85% lines. Thresholds are
glob-specific rather than per-file and are never updated automatically.
Coverage does not replace named boundary, acceptance, or real-provider scenario
matrices.
CI runs four native Vitest shards without retries. Each shard emits a blob
report; the merge job produces aggregate coverage plus JUnit and JSON timing
reports, uploads them together, and writes the slowest files to the workflow
summary. Static checks, documentation, build, package verification, and
real-system lanes remain separate jobs so one kind of evidence cannot stand in
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.
## Apple native metadata and UI
`npm run verify:apple-dispatch` compiles Objective-C class/protocol and Swift
conformance/vtable fixtures, inspects their bytes and repeats after stripping
local symbols. It requires macOS and the host Xcode toolchain; targets are not
executed. `npm run verify:native-ui` launches exactly one source-owned fixture
window and requires successful selected-window capture and approved actions.
Permission denial fails the positive lane. `npm run verify:native-ui:permissions`
allows a permission-boundary-only result and explicitly reports
`positive_e2e: false`; it must not be reported as capture/action proof.
Both commands reject a changed executable digest and clean up the fixture
process and helper. These lanes require an interactive macOS desktop. See [native investigation](native-investigation.md)
for the exact ABI, authority, graph and observation boundaries.
+94
View File
@@ -0,0 +1,94 @@
# MCP tool design
Design tools around analyst tasks. A provider API is an implementation detail;
expose it directly when it gives agents a useful, reusable capability that
cannot be expressed through an existing contract. Do not impose caller-facing
limits merely to make work fit an assumed agent budget. Keep real input-format,
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 |
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.
## Prefer primitives; compose workflows
Start with a primitive when one call can report a reusable fact about one
identified object or relationship, such as an instruction decode, a type
layout, a reference, a dispatch target, or a resource graph. The same primitive
should be useful across different applications or analyst questions. A
format-specific decoder can still be a primitive when it describes a reusable
format rather than one application's product behavior.
Add a workflow when repeated analysis shows that callers need the same
multi-source result and REA can join the evidence without hiding important
choices or uncertainty. A useful check is whether the workflow remains
meaningful for different applications that share the relevant evidence types.
If its purpose depends on one application's business rules or product concepts,
keep that interpretation outside the general tool contract and expose the
underlying evidence through reusable primitives.
## Decide whether to add a tool
1. State the user intent and the smallest result that answers it.
2. Inspect the canonical tool contracts, current provider capabilities, and
representative CLI/MCP traces. Record the nearest competing tool.
3. Extend an existing contract when the intent and result are the same. Add a
batch or composed workflow when it materially improves a demonstrated task;
do not force agents through extra calls or collapse distinct tasks into one
tool. Add a tool when it answers a distinct analyst question or has a
materially different authority or result contract.
4. Keep public tool names and result semantics provider-neutral. Put engine-
specific parsing and protocol handling in adapters. Advertise exact support
per provider; do not create parallel tools just because engines differ.
5. Keep prompts optional and concise. They may point out useful tools, but must
not prescribe a call sequence when the task can be answered directly.
Avoid opaque mode flags and mega-tools that combine unrelated discovery,
execution, and mutation. Prefer a useful direct tool call over a tool sequence
or model-authored loop when one call can answer the question.
## Define the contract
- Use strict object inputs with explicit required fields and enums. Add numeric
bounds only when the format, protocol, authority, or a measured resource
constraint requires them.
- Return complete results by default. Make real pagination, truncation, partial
failure, cancellation, and unavailable states explicit. Missing evidence is
unknown, not empty or false.
- Return task-oriented results inline with artifact identity, provider/version,
addresses or resource paths, Evidence references, confidence, and relevant
limitations. Do not require agents to fetch a resource or dereference an
opaque Evidence link to understand a tool result.
- 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.
- Keep provider-specific types out of provider-neutral domain and application
layers. Normalize supported provider results without implying equal coverage.
## Implement and evaluate
- Put shared user workflows in the application layer; keep MCP translation in
the server adapter and CLI behavior aligned with the same workflow.
- Update the canonical contract inventory, output schemas, examples, generated
catalog, and relevant docs together.
- Verify valid, malformed, boundary, unavailable, cancellation, and partial
results at the owning boundaries. Use real-provider checks for claims that
depend on external analysis engines.
- Evaluate representative broad and direct task prompts on the intended MCP
host when tool selection is the question. Record the host, model-facing tool
catalog, selected tool, arguments, errors, retries, and task outcome.
Schema validity or word-overlap heuristics do not prove discoverability.
See [mcp-contracts.md](mcp-contracts.md) for shipped runtime behavior and
[testing.md](testing.md) for test boundaries and verification lanes.
+6 -8
View File
@@ -12,12 +12,11 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
11,src/domain/errors.ts:ArtifactOperationError,Artifact inventory or extraction fails,"Artifact {operation} failed: {reason} at {logicalPath} (declared_sha256=...)",Exposes paths hashes and internal reason,"Reason-specific plain message with safe recovery step.",implemented,focused assertions passed; full `npm run check` passed
12,src/domain/errors.ts:EvidenceFileError,Evidence file operation fails,"Evidence bundle {operation} failed: {reason}",Internal reason token gives weak recovery,"Reason-specific plain message with safe recovery step.",implemented,focused assertions passed; full `npm run check` passed
13,src/domain/errors.ts:EvidenceIntegrityError,Evidence validation fails,Raw constructor message,May expose validation internals and lacks recovery,"Evidence is invalid or has changed. Recreate or re-import it, then try again.",implemented,focused assertions passed; full `npm run check` passed
14,src/domain/errors.ts:UnknownRegistryError,Evidence-state mutation fails,"Residual unknown registry mutation failed: {reason}",Exposes internal subsystem and state token,"Evidence state changed before the update completed. Refresh the current state and try again.",implemented,focused assertions passed; full `npm run check` passed
14,src/domain/errors.ts:UnknownRegistryError,Evidence-state mutation fails,"Residual unknown registry mutation failed: {reason}",Exposes internal subsystem and state token,"Missing IDs identify the session and retry; concurrent revision conflicts refresh current state.",implemented,focused assertions passed; full `npm run check` passed
15,src/domain/errors.ts:ProcessCaptureError,Process capture fails,Raw constructor message,May expose process and cleanup internals,"Process capture could not complete. Check command access and limits, then try again.",implemented,focused assertions passed; full `npm run check` passed
16,src/domain/errors.ts:AnalysisTimeoutError,Analysis deadline expires,"Analysis operation timed out after {timeoutMs}ms: {operation}",Exposes internal deadline and operation; weak recovery,"Analysis took too long. Try a smaller request, then run `rea doctor` if it continues.",implemented,focused assertions passed; full `npm run check` passed
17,src/domain/errors.ts:HopperTimeoutError,Provider deadline expires,"Hopper did not respond within {timeoutMs}ms",Exposes provider and internal deadline,"Analysis took too long. Try a smaller request, then run `rea doctor` if it continues.",implemented,focused assertions passed; full `npm run check` passed
18,src/domain/errors.ts:ProviderSelectionError,Deep-provider selection fails,"Analysis provider selection failed ({reason}) for {requestedProviderId}; candidates: {candidateIds}",Retains non-secret local selection context that needs a safe structured projection,Reason-specific guidance with stable candidate IDs and actionable rejection diagnostics,implemented,focused registry/CLI/MCP assertions passed; full `npm run check` passed
19,src/domain/errors.ts:EvidenceLimitError,Evidence ledger limit reached,"Evidence ledger {limit} limit exceeded ({maximum})",Exposes internal storage model,"Evidence is too large for this session. Reduce the evidence set and try again.",implemented,focused assertions passed; full `npm run check` passed
20,src/domain/errors.ts:AnalysisInputError,Parsed analysis input invalid,"Invalid analysis input for {operation}",Exposes operation identifier and lacks correction,"Analysis input is invalid. Check the arguments and try again.",implemented,focused assertions passed; full `npm run check` passed
21,src/domain/errors.ts:AnalysisCancelledError,Caller cancels analysis,"Analysis operation was cancelled: {operation}",Exposes operation identifier,"Analysis was cancelled. Start it again when ready.",implemented,focused assertions passed; full `npm run check` passed
22,src/domain/errors.ts:HopperCancelledError,Caller cancels provider request,Hopper operation was cancelled,Exposes provider name,"Analysis was cancelled. Start it again when ready.",implemented,focused assertions passed; full `npm run check` passed
@@ -25,7 +24,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
24,src/server/toolResult.ts:successResult,Successful value violates public output schema,AnalysisOutputError: Analysis output does not match the tool contract,Exposes internal error tag and contract implementation,"Analysis returned an unreadable result. Retry once; if it continues, run `rea doctor`.",implemented,focused assertions passed; full `npm run check` passed
24.5,src/server/registerOfficialTools.ts:capability unknown,Approved unknown tracking records an unavailable analysis,"TOOL_NAME is unavailable: PROVIDER_REASON",Exposes an internal tool identifier and provider diagnostic in persistent user-visible evidence,"The requested analysis is unavailable for the current target.",implemented,verified by focused MCP integration test
24.6,src/application/ReferenceSourceImportEntries.ts:failed entry,A partial reference import contains an unreadable entry,Low-level reader diagnostic stored in entry limitations,Can persist filesystem or reader implementation details,"Code- and entry-specific safe guidance with a retry, limit, exclusion, or platform recovery step.",implemented,verified by exhaustive focused projection test
24.7,src/server/registerSessionTools.ts:process residual,Capture records an approved residual unknown,SCOPE: RAW_REASON,Persists adapter diagnostic and implementation wording,"Was SCOPE behavior fully observed during capture?",implemented,verified by focused session MCP test
24.7,src/server/registerSessionTools.ts:process residual,Capture records a residual unknown linked to capture Evidence,SCOPE: RAW_REASON,Persists adapter diagnostic and implementation wording,"Was SCOPE behavior fully observed during capture?",implemented,verified by focused session MCP test
24.8,src/server/registerInvestigationTools.ts:changed behavior unknown,Changed-behavior workflow remains incomplete,"Changed behavior result EVIDENCE_ID remains STATUS",Persists internal evidence identifier,"Did both versions behave the same under a complete controlled replay?",implemented,verified by focused investigation MCP test
24.9,src/server/registerInvestigationTools.ts:call path unknown,Call-path workflow remains incomplete,Call path START_ADDRESS → GOAL_ADDRESS is STATUS,Persists binary addresses and internal status wording,Can the requested call path be established from complete analysis?,implemented,verified by focused unresolved call-path MCP test
24.91,src/server/registerInvestigationTools.ts:static runtime unknown,Static/runtime correlation remains incomplete,Static/runtime correlation result EVIDENCE_ID is STATUS,Persists internal evidence identifier,Does runtime behavior match the available static analysis?,implemented,verified by focused unresolved static/runtime MCP test
@@ -33,10 +32,10 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
24.93,src/server/registerFunctionComparisonTool.ts:probe rationale,Function comparison is incomplete,Capture complete dossiers with equal providers and analysis limits.,Exposes provider architecture and gives ambiguous equality guidance,"Capture complete dossiers for both functions under the same target context and analysis limits.",implemented,verified by focused function-comparison MCP test
24.95,incur:CLI argument validation,Caller omits or mistypes a command argument,VALIDATION_ERROR with raw Zod Details and fieldErrors including code/path/expected/received,Exposes validator representation and buries the useful correction,"REA could not read the command arguments. Check the named argument and its expected type, then try again.",implemented,verified by sanitizer unit tests and real dispatcher missing-argument test
25,src/application/ReferenceSourceImport.ts:cancelled,Reference-source import is cancelled,Reference source import cancelled,No recovery timing guidance,"Reference source import was cancelled. Start it again when ready.",implemented,focused assertions passed
26,src/application/ReferenceSourceImportPolicy.ts:invalid-limits,Configured import limits are not positive integers,Import limits must be positive integers,Does not tell user where recovery lies,"Reference source limits are invalid. Use positive integer limits, then try again.",implemented,focused assertions passed
26,src/application/ReferenceSourceImportPolicy.ts:invalid-limits,Configured import limits are not positive integers,Import limits must be positive integers,Fixed importer ceilings prevented full source imports,Removed fixed byte, entry, depth, and path limits.,removed,verified by greater-than-10000 entry and greater-than-16-MiB regressions
27,src/application/ReferenceSourceImportPolicy.ts:invalid-root,Source path cannot resolve or is not a directory,Reference source root could not be resolved / Reference source root is not a directory,Multiple low-level copies and no complete path check,"Reference source directory could not be opened. Check that the path exists, is readable, and points to a directory.",implemented,focused assertions passed
28,src/application/ReferenceSourceImportPolicy.ts:policy,No approved root exists or requested root is outside policy,No approved reference source roots / Reference source root is outside approved roots,Does not identify safe configuration recovery,"Reference source directory is not approved. Add its directory to `REA_REFERENCE_ROOTS_JSON`, restart REA, then try again.",implemented,focused assertions passed
29,src/application/ReferenceSourceImport.ts:io,Reader fails during bounded traversal,Raw reader error message,May expose filesystem details and inconsistent recovery,"Reference source files could not be read. Check directory permissions and try again.",implemented,focused assertions passed
28,src/application/ReferenceSourceImportPolicy.ts:policy,No approved root exists or requested root is outside policy,No approved reference source roots / Reference source root is outside approved roots,Approval friction duplicates direct caller path selection,Removed the configured root allowlist and permission gate; caller-selected directories are validated and traversed directly.,removed,verified by source search and focused import tests
29,src/application/ReferenceSourceImport.ts:io,Reader fails during traversal,Raw reader error message,May expose filesystem details and inconsistent recovery,"Reference source files could not be read. Check directory permissions and try again.",implemented,focused assertions passed
30,src/application/ReferenceSourceImport.ts:parse,Historical graph validation fails,Raw thrown graph validation message,Can expose internal graph invariants,"Reference source could not be indexed. Check that the source tree is readable, then try again.",implemented,focused assertions passed
31,src/application/ReferenceSourceImportTypes.ts:limit,Import reports terminal limit failure,No reachable constructor found,Declared state may be unreachable; false verification risk,Remove unreachable top-level limit variant; per-entry limit guidance remains caller-visible.,removed,"verified by typecheck, exhaustive projection test, and source search"
32,src/application/Setup.ts:configureDetectedClients(backup),Agent configuration backup fails,"{clientName} configuration backup verification failed; no successful configuration was reported.",Exposes machine client key and obscures no-write guarantee,"Agent configuration could not be backed up, so no change was made. Check file permissions, then rerun setup.",implemented,focused assertions passed
@@ -73,8 +72,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
62,src/application/ProcessCli.ts:readJson size,Process scenario or evidence file exceeds 64 MiB,Process JSON input exceeds byte limit,Does not state limit or recovery,"Process input file is too large. Reduce it below 64 MiB, then try again.",implemented,verified by focused sparse-file CLI test
63,src/application/ProcessCli.ts:readJson parse,Process scenario or evidence file is malformed JSON,Raw JSON parser exception through CLI framework,Can expose parser implementation and lacks recovery,"Process input file is not valid JSON. Repair the file, then try again.",implemented,verified by focused CLI test
64,src/application/ProcessCli.ts:capture scenario parse,Scenario fails Process Capture schema,Raw Zod validation exception through CLI framework,Exposes schema internals and field paths without stable recovery,"Process scenario is invalid. Check its required fields and limits, then try again.",implemented,verified by focused CLI test
65,src/application/ProcessCli.ts:legacy evidence,Comparison receives Process Capture v3 Evidence,"Process Capture v3 is unsupported. Re-run the scenario with this REA version using capture_process_scenario to produce Process Capture v4; captures cannot be upgraded because v4 requires new manifest and settlement evidence.",Long but accurately explains why recapture is required and gives exact tool recovery,Keep current copy,unchanged,verified by focused CLI test
66,src/application/ProcessCli.ts:wrong evidence,Comparison receives valid Evidence from another workflow,Expected REA Process Capture v4 Evidence,Uses internal schema name and does not explain recovery,"Capture evidence is not from the current process-capture workflow. Create new capture evidence, then try again.",implemented,verified by focused CLI test
66,src/application/ProcessCli.ts:wrong evidence,Comparison receives valid Evidence from another workflow,Expected REA process capture Evidence,Uses internal schema name and does not explain recovery,"Capture evidence is not from the current process-capture workflow. Create new capture evidence, then try again.",implemented,verified by focused CLI test
67,src/application/ProcessCli.ts:unexpected,Unexpected parser/comparison/canonicalization failure escapes expected classifications,Raw thrown exception through CLI framework,Can expose stack or internal invariant,"Process command could not complete. Check the input files and run `rea doctor`, then try again.",implemented,verified by direct unexpected-cause projection test
68,src/server/registerEvidenceTools.ts:object guard,Validated provider-tool input is unexpectedly not an object,Provider tool input was not an object,Exposes provider architecture and implies caller can fix impossible state,Remove the impossible duplicate guard and parse through the declared JSON-object schema.,removed,verified by typecheck and MCP integration tests
69,src/server/registerEnhancedTools.ts:object guard,Validated enhanced-tool input is unexpectedly not an object,Validated enhanced input was not a JSON object,Exposes validation/JSON implementation detail,Remove the impossible duplicate guard and parse through the declared JSON-object schema.,removed,verified by typecheck and enhanced MCP integration tests
1 harm_rank location trigger current_copy user_risk proposed_replacement implementation_status verification_result
12 11 src/domain/errors.ts:ArtifactOperationError Artifact inventory or extraction fails Artifact {operation} failed: {reason} at {logicalPath} (declared_sha256=...) Exposes paths hashes and internal reason Reason-specific plain message with safe recovery step. implemented focused assertions passed; full `npm run check` passed
13 12 src/domain/errors.ts:EvidenceFileError Evidence file operation fails Evidence bundle {operation} failed: {reason} Internal reason token gives weak recovery Reason-specific plain message with safe recovery step. implemented focused assertions passed; full `npm run check` passed
14 13 src/domain/errors.ts:EvidenceIntegrityError Evidence validation fails Raw constructor message May expose validation internals and lacks recovery Evidence is invalid or has changed. Recreate or re-import it, then try again. implemented focused assertions passed; full `npm run check` passed
15 14 src/domain/errors.ts:UnknownRegistryError Evidence-state mutation fails Residual unknown registry mutation failed: {reason} Exposes internal subsystem and state token Evidence state changed before the update completed. Refresh the current state and try again. Missing IDs identify the session and retry; concurrent revision conflicts refresh current state. implemented focused assertions passed; full `npm run check` passed
16 15 src/domain/errors.ts:ProcessCaptureError Process capture fails Raw constructor message May expose process and cleanup internals Process capture could not complete. Check command access and limits, then try again. implemented focused assertions passed; full `npm run check` passed
17 16 src/domain/errors.ts:AnalysisTimeoutError Analysis deadline expires Analysis operation timed out after {timeoutMs}ms: {operation} Exposes internal deadline and operation; weak recovery Analysis took too long. Try a smaller request, then run `rea doctor` if it continues. implemented focused assertions passed; full `npm run check` passed
18 17 src/domain/errors.ts:HopperTimeoutError Provider deadline expires Hopper did not respond within {timeoutMs}ms Exposes provider and internal deadline Analysis took too long. Try a smaller request, then run `rea doctor` if it continues. implemented focused assertions passed; full `npm run check` passed
19 18 src/domain/errors.ts:ProviderSelectionError Deep-provider selection fails Analysis provider selection failed ({reason}) for {requestedProviderId}; candidates: {candidateIds} Retains non-secret local selection context that needs a safe structured projection Reason-specific guidance with stable candidate IDs and actionable rejection diagnostics implemented focused registry/CLI/MCP assertions passed; full `npm run check` passed
19 src/domain/errors.ts:EvidenceLimitError Evidence ledger limit reached Evidence ledger {limit} limit exceeded ({maximum}) Exposes internal storage model Evidence is too large for this session. Reduce the evidence set and try again. implemented focused assertions passed; full `npm run check` passed
20 20 src/domain/errors.ts:AnalysisInputError Parsed analysis input invalid Invalid analysis input for {operation} Exposes operation identifier and lacks correction Analysis input is invalid. Check the arguments and try again. implemented focused assertions passed; full `npm run check` passed
21 21 src/domain/errors.ts:AnalysisCancelledError Caller cancels analysis Analysis operation was cancelled: {operation} Exposes operation identifier Analysis was cancelled. Start it again when ready. implemented focused assertions passed; full `npm run check` passed
22 22 src/domain/errors.ts:HopperCancelledError Caller cancels provider request Hopper operation was cancelled Exposes provider name Analysis was cancelled. Start it again when ready. implemented focused assertions passed; full `npm run check` passed
24 24 src/server/toolResult.ts:successResult Successful value violates public output schema AnalysisOutputError: Analysis output does not match the tool contract Exposes internal error tag and contract implementation Analysis returned an unreadable result. Retry once; if it continues, run `rea doctor`. implemented focused assertions passed; full `npm run check` passed
25 24.5 src/server/registerOfficialTools.ts:capability unknown Approved unknown tracking records an unavailable analysis TOOL_NAME is unavailable: PROVIDER_REASON Exposes an internal tool identifier and provider diagnostic in persistent user-visible evidence The requested analysis is unavailable for the current target. implemented verified by focused MCP integration test
26 24.6 src/application/ReferenceSourceImportEntries.ts:failed entry A partial reference import contains an unreadable entry Low-level reader diagnostic stored in entry limitations Can persist filesystem or reader implementation details Code- and entry-specific safe guidance with a retry, limit, exclusion, or platform recovery step. implemented verified by exhaustive focused projection test
27 24.7 src/server/registerSessionTools.ts:process residual Capture records an approved residual unknown Capture records a residual unknown linked to capture Evidence SCOPE: RAW_REASON Persists adapter diagnostic and implementation wording Was SCOPE behavior fully observed during capture? implemented verified by focused session MCP test
28 24.8 src/server/registerInvestigationTools.ts:changed behavior unknown Changed-behavior workflow remains incomplete Changed behavior result EVIDENCE_ID remains STATUS Persists internal evidence identifier Did both versions behave the same under a complete controlled replay? implemented verified by focused investigation MCP test
29 24.9 src/server/registerInvestigationTools.ts:call path unknown Call-path workflow remains incomplete Call path START_ADDRESS → GOAL_ADDRESS is STATUS Persists binary addresses and internal status wording Can the requested call path be established from complete analysis? implemented verified by focused unresolved call-path MCP test
30 24.91 src/server/registerInvestigationTools.ts:static runtime unknown Static/runtime correlation remains incomplete Static/runtime correlation result EVIDENCE_ID is STATUS Persists internal evidence identifier Does runtime behavior match the available static analysis? implemented verified by focused unresolved static/runtime MCP test
32 24.93 src/server/registerFunctionComparisonTool.ts:probe rationale Function comparison is incomplete Capture complete dossiers with equal providers and analysis limits. Exposes provider architecture and gives ambiguous equality guidance Capture complete dossiers for both functions under the same target context and analysis limits. implemented verified by focused function-comparison MCP test
33 24.95 incur:CLI argument validation Caller omits or mistypes a command argument VALIDATION_ERROR with raw Zod Details and fieldErrors including code/path/expected/received Exposes validator representation and buries the useful correction REA could not read the command arguments. Check the named argument and its expected type, then try again. implemented verified by sanitizer unit tests and real dispatcher missing-argument test
34 25 src/application/ReferenceSourceImport.ts:cancelled Reference-source import is cancelled Reference source import cancelled No recovery timing guidance Reference source import was cancelled. Start it again when ready. implemented focused assertions passed
35 26 src/application/ReferenceSourceImportPolicy.ts:invalid-limits Configured import limits are not positive integers Import limits must be positive integers Does not tell user where recovery lies Fixed importer ceilings prevented full source imports Reference source limits are invalid. Use positive integer limits, then try again. Removed fixed byte implemented entry focused assertions passed depth
36 27 src/application/ReferenceSourceImportPolicy.ts:invalid-root Source path cannot resolve or is not a directory Reference source root could not be resolved / Reference source root is not a directory Multiple low-level copies and no complete path check Reference source directory could not be opened. Check that the path exists, is readable, and points to a directory. implemented focused assertions passed
37 28 src/application/ReferenceSourceImportPolicy.ts:policy No approved root exists or requested root is outside policy No approved reference source roots / Reference source root is outside approved roots Does not identify safe configuration recovery Approval friction duplicates direct caller path selection Reference source directory is not approved. Add its directory to `REA_REFERENCE_ROOTS_JSON`, restart REA, then try again. Removed the configured root allowlist and permission gate; caller-selected directories are validated and traversed directly. implemented removed focused assertions passed verified by source search and focused import tests
38 29 src/application/ReferenceSourceImport.ts:io Reader fails during bounded traversal Reader fails during traversal Raw reader error message May expose filesystem details and inconsistent recovery Reference source files could not be read. Check directory permissions and try again. implemented focused assertions passed
39 30 src/application/ReferenceSourceImport.ts:parse Historical graph validation fails Raw thrown graph validation message Can expose internal graph invariants Reference source could not be indexed. Check that the source tree is readable, then try again. implemented focused assertions passed
40 31 src/application/ReferenceSourceImportTypes.ts:limit Import reports terminal limit failure No reachable constructor found Declared state may be unreachable; false verification risk Remove unreachable top-level limit variant; per-entry limit guidance remains caller-visible. removed verified by typecheck, exhaustive projection test, and source search
41 32 src/application/Setup.ts:configureDetectedClients(backup) Agent configuration backup fails {clientName} configuration backup verification failed; no successful configuration was reported. Exposes machine client key and obscures no-write guarantee Agent configuration could not be backed up, so no change was made. Check file permissions, then rerun setup. implemented focused assertions passed
72 62 src/application/ProcessCli.ts:readJson size Process scenario or evidence file exceeds 64 MiB Process JSON input exceeds byte limit Does not state limit or recovery Process input file is too large. Reduce it below 64 MiB, then try again. implemented verified by focused sparse-file CLI test
73 63 src/application/ProcessCli.ts:readJson parse Process scenario or evidence file is malformed JSON Raw JSON parser exception through CLI framework Can expose parser implementation and lacks recovery Process input file is not valid JSON. Repair the file, then try again. implemented verified by focused CLI test
74 64 src/application/ProcessCli.ts:capture scenario parse Scenario fails Process Capture schema Raw Zod validation exception through CLI framework Exposes schema internals and field paths without stable recovery Process scenario is invalid. Check its required fields and limits, then try again. implemented verified by focused CLI test
75 65 66 src/application/ProcessCli.ts:legacy evidence src/application/ProcessCli.ts:wrong evidence Comparison receives Process Capture v3 Evidence Comparison receives valid Evidence from another workflow Process Capture v3 is unsupported. Re-run the scenario with this REA version using capture_process_scenario to produce Process Capture v4; captures cannot be upgraded because v4 requires new manifest and settlement evidence. Expected REA process capture Evidence Long but accurately explains why recapture is required and gives exact tool recovery Uses internal schema name and does not explain recovery Keep current copy Capture evidence is not from the current process-capture workflow. Create new capture evidence, then try again. unchanged implemented verified by focused CLI test
66 src/application/ProcessCli.ts:wrong evidence Comparison receives valid Evidence from another workflow Expected REA Process Capture v4 Evidence Uses internal schema name and does not explain recovery Capture evidence is not from the current process-capture workflow. Create new capture evidence, then try again. implemented verified by focused CLI test
76 67 src/application/ProcessCli.ts:unexpected Unexpected parser/comparison/canonicalization failure escapes expected classifications Raw thrown exception through CLI framework Can expose stack or internal invariant Process command could not complete. Check the input files and run `rea doctor`, then try again. implemented verified by direct unexpected-cause projection test
77 68 src/server/registerEvidenceTools.ts:object guard Validated provider-tool input is unexpectedly not an object Provider tool input was not an object Exposes provider architecture and implies caller can fix impossible state Remove the impossible duplicate guard and parse through the declared JSON-object schema. removed verified by typecheck and MCP integration tests
78 69 src/server/registerEnhancedTools.ts:object guard Validated enhanced-tool input is unexpectedly not an object Validated enhanced input was not a JSON object Exposes validation/JSON implementation detail Remove the impossible duplicate guard and parse through the declared JSON-object schema. removed verified by typecheck and enhanced MCP integration tests
@@ -1,103 +1,102 @@
{
"schema_version": 1,
"ledger_id": "ecl_a56a75b91375c9c9501d1545fee9977e74f57dbd1bf413a370f9cd1ed26987bd",
"ledger_id": "ecl_b01c0b28362b48128765d176113471a5a82637fca0acfcd3182971155942e0d4",
"records": [
{
"claim_id": "managed.application-graph-projection",
"status": "pass",
"evidence_ids": [
"ev_4d6821cdd212a77507dfbdf995fc8b086cd63b3f1d39dee4a2e12f62a6f0609d"
"ev_b061a5a559acb3748cb9784c637f6693f74faeb564ba2b11d6fc32619e757459"
]
},
{
"claim_id": "managed.decompiler-reconstruction-import",
"status": "pass",
"evidence_ids": [
"ev_cd9015f39a60251bee1b01baf5189de7427e5810f130aed39770f17ae69cee44"
"ev_13f3401a3aaddc3f6d087ad160fa928374e142cdb9861254359b46865b8c992e"
]
},
{
"claim_id": "managed.dotnet-framework-x86-pinvoke",
"status": "pass",
"evidence_ids": [
"ev_af02c0218a01138bd1f0f5ca052f95d348dfacb540040ab72d6ddc9889e3a7e4"
"ev_6f026216826ca06e080b77ff608f02634fa945fbd2e151b4cd261c1444f8bbb1"
]
},
{
"claim_id": "managed.ilspy-reconstruction-oracle",
"status": "unsupported",
"evidence_ids": [
"ev_5df12074aeb26d67bc954fd9230076bd0c3ce9969600e19c59ba02090a9988f2"
"ev_65a94c4de03f9c6ebdaff3f381cca377821f349d3ca6dfad47a31cc586fae939"
]
},
{
"claim_id": "managed.malformed-metadata",
"status": "pass",
"evidence_ids": [
"ev_5d6aadf199ae090e0ddebc10a51aab8a410fa778a6ae8454fedfb10d240cdac2"
"ev_deae4259ea4395623176de40d06c2abcbc2b3a77f0bfb068d76e568d801f45a8"
]
},
{
"claim_id": "managed.managed-native-verification",
"status": "pass",
"evidence_ids": [
"ev_9f1862276fe8869b86105eb91154603922eeb95fec33a10c354b2d6c9c3fe077"
"ev_0edcbbcaa1191a4e5324a437f739957b77f9ac45e7897724337f2d6d06b3f9d1"
]
},
{
"claim_id": "managed.modern-dotnet-anycpu",
"status": "pass",
"evidence_ids": [
"ev_c9aef74724cf555bcedce8a9dbf46327b2f92b95b03eb363f966885d429e6882"
"ev_d1af50e8dc04cd589385769efd8758e82095425f6e9c1cc4bf2f530c6b434c93"
]
},
{
"claim_id": "managed.mvid-and-token-drift",
"status": "pass",
"evidence_ids": [
"ev_55808bf3ee5ec9d87cee751462077a6bce88701c4fcf66822ba1d7d33706706b"
"ev_5493f40bc875535488d789a490361abcc1f76bfbdf52536d09ff127442a8e854"
]
},
{
"claim_id": "managed.not-managed",
"status": "pass",
"evidence_ids": [
"ev_a8166506ca91a4595b825f355836bb4becf35852c8547c55408c09698f4953a6"
"ev_03ce0cc0183ee0710f8120e837565129559ab3bc41927df326066370c2341699"
]
},
{
"claim_id": "managed.operator-local-manifest",
"status": "unsupported",
"evidence_ids": [
"ev_03d1a151446834fbee7c093e82121b714a0cc1e6167ad84f47a79ffb9e2602ce"
"ev_9d3e50da1d372c55f46649e69eae702cbe8f0ba6a81d1e46afc3e1a284c81757"
]
},
{
"claim_id": "managed.operator-manifest-self-test",
"status": "pass",
"evidence_ids": [
"ev_74d7f9c75a4476196664542d790e2532435f2a1c54349b481967e51e2ccc9453"
"ev_f974f4b9d039cda841894c6173cae86427ecef48f4e7a4d6c73edf7b23af2b2a"
]
},
{
"claim_id": "managed.runtime-correlation-admission-plan",
"status": "pass",
"evidence_ids": [
"ev_5a80df0b92dc2280361e900ce590bc88ea52eba3dc8e216a0ce282f503e3cb05"
"ev_3e546c20493faf9696502171a5cfe9504569cffb22f0561ee3000f0027b9bbd8"
]
},
{
"claim_id": "managed.unicode-obfuscated-identifiers",
"status": "pass",
"evidence_ids": [
"ev_3b61c79ec152c70693278a70ad2ca6528ce0a9c9ab389d3ec9e4520de79082a7"
"ev_c4f38ce76d8359d92ee8e9442503e47b7913550442349a0df688bd274a3def67"
]
},
{
"claim_id": "managed.x64-ready-to-run-native-body",
"status": "pass",
"evidence_ids": [
"ev_81f41b1b3809c2fe1964f7abbcbd2faeb14c723938639c2e0bb16cd58746aa1a"
"ev_3e05ef1d6a796d3d4cb3372fe8e549efec759c59bf09d8ff660059a2e448cbc0"
]
}
],
@@ -1,20 +1,19 @@
{
"schema_version": 1,
"manifest_id": "ecm_ee8e5f2f4ea090052051d2994cbff148d9c796e1bc28b2fdc145aa5fa48cd248",
"manifest_id": "ecm_fcfc6a2f890ccb4a5a7d19dce941b8f7644f05babd89e223100ef7b8b7a55afc",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
},
"environment": {
"platform": "linux",
"architecture": "x64",
"platform": "portable",
"architecture": "portable",
"runtime": "node",
"runtime_version": "24"
"runtime_version": "portable"
},
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "6a910f067c946d545f5eb711eacbdf804839dbfb6b3b7b6ea91769be37ae47ce"
"sha256": "6a884e8e2df17da0df0e8fda01aa3d66726fd7454c5606752c2b3ba3aa9e5449"
}
],
"claims": [
@@ -30,8 +29,7 @@
"provider": {
"id": "rea-managed-conformance",
"version": "1"
},
"result_schema_version": 1
}
},
{
"claim_id": "managed.decompiler-reconstruction-import",
@@ -45,8 +43,7 @@
"provider": {
"id": "rea-managed-conformance",
"version": "1"
},
"result_schema_version": 1
}
},
{
"claim_id": "managed.dotnet-framework-x86-pinvoke",
@@ -60,8 +57,7 @@
"provider": {
"id": "rea-managed-conformance",
"version": "1"
},
"result_schema_version": 1
}
},
{
"claim_id": "managed.ilspy-reconstruction-oracle",
@@ -73,8 +69,7 @@
"provider": {
"id": "rea-managed-conformance",
"version": "1"
},
"result_schema_version": 1
}
},
{
"claim_id": "managed.malformed-metadata",
@@ -88,8 +83,7 @@
"provider": {
"id": "rea-managed-conformance",
"version": "1"
},
"result_schema_version": 1
}
},
{
"claim_id": "managed.managed-native-verification",
@@ -104,8 +98,7 @@
"provider": {
"id": "rea-managed-conformance",
"version": "1"
},
"result_schema_version": 1
}
},
{
"claim_id": "managed.modern-dotnet-anycpu",
@@ -119,8 +112,7 @@
"provider": {
"id": "rea-managed-conformance",
"version": "1"
},
"result_schema_version": 1
}
},
{
"claim_id": "managed.mvid-and-token-drift",
@@ -135,8 +127,7 @@
"provider": {
"id": "rea-managed-conformance",
"version": "1"
},
"result_schema_version": 1
}
},
{
"claim_id": "managed.not-managed",
@@ -150,8 +141,7 @@
"provider": {
"id": "rea-managed-conformance",
"version": "1"
},
"result_schema_version": 1
}
},
{
"claim_id": "managed.operator-local-manifest",
@@ -163,8 +153,7 @@
"provider": {
"id": "rea-managed-conformance",
"version": "1"
},
"result_schema_version": 1
}
},
{
"claim_id": "managed.operator-manifest-self-test",
@@ -178,8 +167,7 @@
"provider": {
"id": "rea-managed-conformance",
"version": "1"
},
"result_schema_version": 1
}
},
{
"claim_id": "managed.runtime-correlation-admission-plan",
@@ -193,8 +181,7 @@
"provider": {
"id": "rea-managed-conformance",
"version": "1"
},
"result_schema_version": 1
}
},
{
"claim_id": "managed.unicode-obfuscated-identifiers",
@@ -208,8 +195,7 @@
"provider": {
"id": "rea-managed-conformance",
"version": "1"
},
"result_schema_version": 1
}
},
{
"claim_id": "managed.x64-ready-to-run-native-body",
@@ -223,8 +209,7 @@
"provider": {
"id": "rea-managed-conformance",
"version": "1"
},
"result_schema_version": 1
}
}
]
}
+7 -7
View File
@@ -10,7 +10,7 @@ The P0 accepts exactly:
- a Windows x64 host;
- Node.js 22.19+ or 24.11+;
- an operator-installed official Ghidra 12.1.2 distribution;
- 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.
@@ -31,7 +31,7 @@ Install REA, Ghidra, and JDK 21 separately. In PowerShell:
```powershell
npm install --global rea-agents
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.2_PUBLIC"
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
$env:JAVA_HOME = "C:\Program Files\Java\jdk-21"
$env:REA_ANALYSIS_PROVIDER = "ghidra"
@@ -58,7 +58,7 @@ three environment variables above. A representative configuration is:
],
"env": {
"REA_ANALYSIS_PROVIDER": "ghidra",
"GHIDRA_INSTALL_DIR": "C:\\tools\\ghidra_12.1.2_PUBLIC",
"GHIDRA_INSTALL_DIR": "C:\\tools\\ghidra_12.1.4_PUBLIC",
"JAVA_HOME": "C:\\Program Files\\Java\\jdk-21"
}
}
@@ -95,8 +95,8 @@ and no verified no-follow equivalent for every path component.
Linux continues to use a mode-restricted Unix socket. Windows uses a Java
listener bound to `127.0.0.1` on an ephemeral port. The bridge atomically
publishes a bounded endpoint record containing only schema version, literal
loopback host, and port. The random 256-bit request token remains only in the
publishes a bounded endpoint record containing only the literal loopback host
and port. The random 256-bit request token remains only in the
ephemeral session descriptor and every request is authenticated.
`analyzeHeadless.bat` is invoked through an absolute `cmd.exe` with a fixed
@@ -138,14 +138,14 @@ and compares the packaged MCP catalog with `TOOL_CONTRACTS`.
The controlled real-engine lane is intentionally separate:
```powershell
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.2_PUBLIC"
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
$env:JAVA_HOME = "C:\Program Files\Java\jdk-21"
npm ci
npm run verify:ghidra:windows
```
The verifier creates an ignored deterministic x86-64 PE fixture, checks its
fixed SHA-256, opens Ghidra 12.1.2 through the production Java bridge, exercises
fixed SHA-256, opens Ghidra 12.1.4 through the production Java bridge, exercises
all 19 admitted operations, verifies the target/snapshot/import digest chain,
and checks endpoint, project, process, and runtime cleanup. The GitHub workflow
accepts only the fixed `real-ghidra-windows` repository-dispatch event against
+1 -1
View File
@@ -102,7 +102,7 @@ printf 'Installing %s@%s...\n' "$PACKAGE" "$version"
npm install --global "${prefix_args[@]}" "$PACKAGE@$version" || fail "npm could not install REA. Check registry access and npm permissions, then retry."
[[ -x "$install_bin" ]] || fail "npm completed without installing the rea command. Check the npm global bin directory and PATH, then retry."
installed_version="$("$install_bin" --version 2>/dev/null | tr -d '[:space:]')" || fail "the installed REA version could not be read. Reinstall the requested version, then retry."
[[ "$installed_version" == "$version" ]] || fail "installed version $installed_version does not match $version."
[[ "$installed_version" == "$version" ]] || fail "installed version $installed_version does not match $version. Reinstall the requested version, then retry."
printf 'REA %s is installed.\n' "$version"
if [[ "$start_setup" == true && -r /dev/tty && -w /dev/tty && -t 1 ]]; then
+8 -2
View File
@@ -1,10 +1,16 @@
{
"$schema": "./node_modules/knip/schema.json",
"entry": ["src/main.ts", "src/cli.ts"],
"entry": ["src/main.ts", "src/cli.ts", "src/contracts/errorSchemas.ts"],
"project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"],
"ignoreDependencies": [],
"ignoreDependencies": ["electron"],
"ignoreBinaries": ["ps"],
"ignoreExportsUsedInFile": true,
"ignoreIssues": {
"src/domain/mobileApplicationInvestigation.ts": ["types"],
"src/domain/objcSwiftMetadata.ts": ["types"],
"src/domain/interfaceBuilderGraph.ts": ["types"],
"src/domain/nativeInvestigationGraph.ts": ["types"]
},
"rules": {
"files": "error",
"dependencies": "error",
+76 -16
View File
@@ -1,12 +1,12 @@
{
"name": "rea-agents",
"version": "2.5.1",
"version": "3.2.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rea-agents",
"version": "2.5.1",
"version": "3.2.1",
"license": "MIT",
"dependencies": {
"@babel/parser": "8.0.4",
@@ -15,8 +15,8 @@
"@electron/asar": "4.2.0",
"@jridgewell/trace-mapping": "0.3.31",
"@lydell/node-pty": "1.1.0",
"@modelcontextprotocol/client": "2.0.0-beta.4",
"@modelcontextprotocol/server": "2.0.0-beta.4",
"@modelcontextprotocol/client": "2.0.0",
"@modelcontextprotocol/server": "2.0.0",
"@xterm/addon-serialize": "0.14.0",
"@xterm/headless": "6.0.0",
"@zip.js/zip.js": "2.8.26",
@@ -47,6 +47,7 @@
"@types/write-file-atomic": "4.0.3",
"@types/ws": "8.18.1",
"@vitest/coverage-v8": "4.1.10",
"ajv": "8.20.0",
"husky": "9.1.7",
"jscpd": "5.0.12",
"knip": "6.26.0",
@@ -56,7 +57,8 @@
"turbo": "2.10.5",
"typedoc": "0.28.20",
"typescript": "5.9.3",
"vitest": "4.1.10"
"vitest": "4.1.10",
"yaml": "2.9.0"
},
"engines": {
"node": "^22.19.0 || >=24.11.0"
@@ -369,12 +371,12 @@
]
},
"node_modules/@modelcontextprotocol/client": {
"version": "2.0.0-beta.4",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0-beta.4.tgz",
"integrity": "sha512-VNHA/UXDk7mCpVl+jOg5B4WMRRD2OEl+it360Lhi6HiCbrIB2f6pZ0cqSDKXQVUtZvqnhdQHzZAM9h8EKWhq/A==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0.tgz",
"integrity": "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw==",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/core": "2.0.0-beta.4",
"@modelcontextprotocol/core": "2.0.0",
"cross-spawn": "^7.0.5",
"eventsource": "^3.0.2",
"eventsource-parser": "^3.0.0",
@@ -387,9 +389,9 @@
}
},
"node_modules/@modelcontextprotocol/core": {
"version": "2.0.0-beta.4",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0-beta.4.tgz",
"integrity": "sha512-nsMXd4wQBKzmph6r+WOhum+mXjDYljTAqwY/XUg3hLtvNOQ8+JVqBSJOVCMJvx9lXhTpTOPrGZ3BuNiaNPjSvg==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz",
"integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==",
"license": "MIT",
"dependencies": {
"zod": "^4.2.0"
@@ -399,12 +401,12 @@
}
},
"node_modules/@modelcontextprotocol/server": {
"version": "2.0.0-beta.4",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0-beta.4.tgz",
"integrity": "sha512-pjMZcNEt1dOq0aJCcY3b7w1Ayh+qmQN2xlfTELcGV9yiAjEGIczBPBLWWW0k0zzo5T8kiv15jtKPsWeHGxLvLg==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz",
"integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/core": "2.0.0-beta.4",
"@modelcontextprotocol/core": "2.0.0",
"zod": "^4.2.0"
},
"engines": {
@@ -2157,6 +2159,23 @@
"node": ">=6.5"
}
},
"node_modules/ajv": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ansi-escapes": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
@@ -2711,6 +2730,13 @@
"node": ">=12.17.0"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-string-truncated-width": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz",
@@ -2726,6 +2752,23 @@
"fast-string-truncated-width": "^3.0.2"
}
},
"node_modules/fast-uri": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
"integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "BSD-3-Clause"
},
"node_modules/fast-wrap-ansi": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz",
@@ -3375,6 +3418,13 @@
"win32"
]
},
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/knip": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/knip/-/knip-6.26.0.tgz",
@@ -4474,6 +4524,16 @@
"node": ">= 12.13.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+48 -18
View File
@@ -1,6 +1,7 @@
{
"name": "rea-agents",
"version": "2.5.1",
"mcpName": "io.github.morluto/rea",
"version": "3.2.1",
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
"license": "MIT",
"repository": {
@@ -41,8 +42,11 @@
"dist",
"install.sh",
"bridge/hopper_bridge.py",
"bridge/native/ReaNativeUI.swift",
"bridge/ghidra/ReaGhidraBridge.java",
"scripts/rea.mjs",
"scripts/electron-active-hook.cjs",
"scripts/electron-active-hook-boundaries.cjs",
"scripts/hopper-demo-x11.py",
"scripts/prepack.mjs",
"scripts/prepare.mjs",
@@ -54,16 +58,21 @@
"deps:check": "node scripts/check-dependency-install.mjs",
"metadata:generate": "node scripts/generate-package-metadata.mjs",
"metadata:check": "node scripts/generate-package-metadata.mjs --check",
"prebuild": "npm run deps:check && npm run metadata:check",
"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",
"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",
"precheck:changed": "npm run deps:check",
"check:changed": "turbo run typecheck lint && npm run test:changed",
"precheck:test": "npm run deps:check",
"check:test": "turbo run typecheck lint format:check knip metadata:check test:run",
"precheck:pr": "npm run deps:check",
"check:pr": "turbo run typecheck lint format:check knip metadata:check test:run docs:check:built",
"check:pr": "turbo run typecheck lint format:check knip metadata:check test:run && npm run docs:check",
"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",
@@ -76,32 +85,48 @@
"jscpd": "jscpd src/ tests/",
"scan:todos": "node scripts/scan-todos.mjs",
"start": "node scripts/rea.mjs mcp",
"pretest": "npm run build:cached",
"test": "vitest run",
"test:run": "vitest run",
"test": "node scripts/run-exclusive.mjs 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 parallel --project subprocess",
"test:fast": "vitest run --project domain --project services --project adapters --project composition --project boundary --project process-boundary --project mcp-boundary --project conformance",
"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 parallel --project subprocess --changed",
"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",
"pretest:integration": "npm run build:cached",
"test:integration": "vitest run --project serial-integration",
"test:integration": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global --project acceptance",
"pretest:coverage": "npm run build:cached",
"test:coverage": "vitest run --coverage.enabled --coverage.provider=v8",
"test:ci": "npm run test:coverage -- --reporter=default --reporter=junit --outputFile.junit=test-results.xml",
"pretest:ci:shard": "npm run build:cached",
"test:ci:shard": "npm run test:ci:shard:run",
"test:ci:shard:run": "vitest run --coverage.enabled --coverage.provider=v8 --coverage.thresholds.lines=0 --coverage.thresholds.functions=0 --coverage.thresholds.branches=0 --coverage.thresholds.statements=0 --reporter=default --reporter=blob",
"test:ci:merge": "vitest --merge-reports --coverage.enabled --coverage.provider=v8 --reporter=default --reporter=junit --outputFile.junit=test-results.xml",
"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 parallel --project subprocess --changed",
"test:watch": "vitest --project domain --project services --project adapters --project composition --project boundary --project process-boundary --project mcp-boundary --project conformance --project evaluation --changed",
"pretest:watch:all": "npm run build:cached",
"test:watch:all": "vitest --changed",
"typecheck": "tsc --noEmit",
"verify:test-temp-hygiene": "npm run build:cached && node scripts/verify-test-temp-hygiene.mjs",
"verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs",
"verify:apple-dispatch": "npm run build:cached && node scripts/verify-apple-dispatch.mjs",
"verify:native-ui": "npm run build:cached && node scripts/verify-native-ui.mjs",
"verify:keyed-archive": "npm run build:cached && node scripts/verify-keyed-archive.mjs",
"verify:ghidra": "npm run build:cached && node scripts/verify-real-ghidra.mjs",
"verify:ghidra:aarch64-jump-table": "npm run build:cached && node scripts/verify-real-ghidra.mjs --aarch64-jump-table",
"verify:interface-builder": "npm run build:cached && node scripts/verify-interface-builder.mjs",
"verify:ghidra:cross-format": "npm run build:cached && node scripts/verify-real-ghidra.mjs --cross-format",
"verify:ghidra:windows": "npm run build:cached && npm run build:ghidra-windows-fixture && node scripts/verify-real-ghidra-windows.mjs",
"verify:browser": "npm run build:cached && node scripts/verify-real-browser.mjs",
"verify:electron": "npm run build:cached && node scripts/verify-real-electron.mjs",
"verify:inspector": "npm run build:cached && node scripts/verify-real-javascript-runtime-observation.mjs",
"verify:javascript-runtime": "npm run build:cached && node scripts/verify-local-javascript-runtime.mjs",
"verify:application-workflows": "npm run build:cached && node scripts/verify-local-application-workflows.mjs",
@@ -123,12 +148,15 @@
"evidence:check": "npm run build:cached && node scripts/generate-completion-ledger.mjs --check",
"docs:api": "typedoc",
"docs:api:cached": "turbo run docs:api",
"docs:generate": "npm run metadata:generate && npm run build:cached && 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:check": "turbo run docs:check:built",
"docs:check:built": "node scripts/generate-package-metadata.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: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:check": "node scripts/run-exclusive.mjs docs turbo run docs:check:built",
"docs:check:built": "node scripts/generate-package-metadata.mjs --check && node scripts/generate-mcp-tool-catalog.mjs --check && node scripts/generate-skill-metadata.mjs --check && node scripts/generate-product-catalog.mjs --check && node scripts/generate-completion-ledger.mjs --check && node scripts/check-doc-facts.mjs && node scripts/generate-error-schema.mjs --check",
"docs:error-schema": "npm run build:cached && node scripts/generate-error-schema.mjs",
"prepack": "node scripts/prepack.mjs",
"prepare": "node scripts/prepare.mjs"
"prepare": "node scripts/prepare.mjs",
"verify:asset-catalog": "npm run build:cached && node scripts/verify-asset-catalog.mjs",
"verify:native-ui:permissions": "npm run build:cached && node scripts/verify-native-ui.mjs --allow-permission-denial"
},
"lint-staged": {
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
@@ -144,8 +172,8 @@
"@electron/asar": "4.2.0",
"@jridgewell/trace-mapping": "0.3.31",
"@lydell/node-pty": "1.1.0",
"@modelcontextprotocol/client": "2.0.0-beta.4",
"@modelcontextprotocol/server": "2.0.0-beta.4",
"@modelcontextprotocol/client": "2.0.0",
"@modelcontextprotocol/server": "2.0.0",
"@xterm/addon-serialize": "0.14.0",
"@xterm/headless": "6.0.0",
"@zip.js/zip.js": "2.8.26",
@@ -172,6 +200,7 @@
"@types/write-file-atomic": "4.0.3",
"@types/ws": "8.18.1",
"@vitest/coverage-v8": "4.1.10",
"ajv": "8.20.0",
"husky": "9.1.7",
"jscpd": "5.0.12",
"knip": "6.26.0",
@@ -181,6 +210,7 @@
"turbo": "2.10.5",
"typedoc": "0.28.20",
"typescript": "5.9.3",
"vitest": "4.1.10"
"vitest": "4.1.10",
"yaml": "2.9.0"
}
}
+10
View File
@@ -25,6 +25,16 @@
"type": "generic",
"path": "src/generatedPackageMetadata.ts"
},
{
"type": "json",
"path": "server.json",
"jsonpath": "$.version"
},
{
"type": "json",
"path": "server.json",
"jsonpath": "$.packages[0].version"
},
{
"type": "json",
"path": "docs/product-catalog.json",
+2 -3
View File
@@ -37,7 +37,6 @@ await buildPortableFixtures();
if (hostPlatform === "darwin") await buildDarwinFixtures();
const manifest = {
schemaVersion: 1,
platform: hostPlatform,
architecture,
generatedAt: new Date(0).toISOString(),
@@ -103,9 +102,9 @@ async function buildPortableFixtures() {
join(outputRoot, "large"),
],
expectations: {
symbolPrefix: `${symbolPrefix}rea_page_`,
symbolPrefix: `${symbolPrefix}rea_inventory_`,
symbolCount: LARGE_FIXTURE_COUNT,
stringPrefix: "REA_PAGE_",
stringPrefix: "REA_INVENTORY_",
stringCount: LARGE_FIXTURE_COUNT,
},
});
+10
View File
@@ -0,0 +1,10 @@
import { rm } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
await Promise.all([
rm(join(root, "dist"), { recursive: true, force: true }),
rm(join(root, ".cache", "tsconfig.build.tsbuildinfo"), { force: true }),
]);
+394
View File
@@ -0,0 +1,394 @@
"use strict";
// oxlint-disable-next-line max-lines-per-function -- all adapters must close over one bounded capture context.
const createElectronActiveBoundaryPatches = ({
identity,
patchEmitter,
processType,
recordIpc,
recordRuntime,
shape,
}) => {
const patchedSessions = new WeakSet();
const patchIpcRenderer = (ipcRenderer) => {
if (ipcRenderer === null || ipcRenderer === undefined) return;
for (const [method, kind] of [
["send", "ipc-renderer-send"],
["sendSync", "ipc-renderer-send"],
["invoke", "ipc-renderer-invoke"],
["postMessage", "ipc-renderer-post-message"],
]) {
const original = ipcRenderer[method];
if (typeof original !== "function") continue;
ipcRenderer[method] = function patchedRendererIpc(channel, ...args) {
const recorded = recordIpc(kind, channel, args, {
direction: "renderer-to-main",
sender: processType(),
receiver: "main",
frame: null,
process_type: processType(),
error: false,
});
try {
const result = original.call(this, channel, ...args);
if (result !== null && typeof result?.then === "function")
return result.then(
(value) => {
if (recorded !== null) recorded.result_shape = shape(value);
return value;
},
(cause) => {
if (recorded !== null) recorded.error = true;
throw cause;
},
);
if (recorded !== null) recorded.result_shape = shape(result);
return result;
} catch (cause) {
if (recorded !== null) recorded.error = true;
throw cause;
}
};
}
};
const patchContextBridge = (contextBridge) => {
if (contextBridge === null || contextBridge === undefined) return;
for (const method of ["exposeInMainWorld", "exposeInIsolatedWorld"]) {
const original = contextBridge[method];
if (typeof original !== "function") continue;
contextBridge[method] = function patchedContextBridge(...args) {
const target = typeof args[0] === "string" ? args[0] : null;
recordRuntime("preload", "context-bridge-exposure", "attempted", {
target,
argument_shapes: args.map((value) => shape(value)),
process_type: processType(),
});
try {
const result = original.apply(this, args);
recordRuntime("preload", "context-bridge-exposure", "completed", {
target,
process_type: processType(),
});
return result;
} catch (cause) {
recordRuntime("preload", "context-bridge-exposure", "failed", {
target,
error: true,
process_type: processType(),
});
throw cause;
}
};
}
};
const patchSession = (session) => {
if (session === null || session === undefined) return;
if (
(typeof session !== "object" && typeof session !== "function") ||
patchedSessions.has(session)
)
return;
patchedSessions.add(session);
patchEmitter(session, (event, args) => {
const kind =
event === "will-download"
? "download"
: event.includes("certificate") || event.includes("permission")
? "permission"
: event.includes("window") || event.includes("popup")
? "popup-attempt"
: "web-contents-lifecycle";
if (event === "will-download") {
const cancel = args[1]?.cancel;
if (typeof cancel === "function") cancel.call(args[1]);
}
recordRuntime(
kind,
event,
event === "will-download" ? "blocked" : "observed",
{
target: identity(args[0], "webContents"),
argument_shapes: args.map((value) => shape(value)),
error: event === "will-download",
},
);
});
const originalPermissionRequest = session.setPermissionRequestHandler;
if (typeof originalPermissionRequest === "function")
session.setPermissionRequestHandler =
function blockedPermissionRequest() {
return originalPermissionRequest.call(
this,
(webContents, permission, callback) => {
recordRuntime("permission", "permission-request", "blocked", {
target: identity(webContents, "webContents"),
argument_shapes: [shape(permission)],
error: true,
});
if (typeof callback === "function") callback(false);
},
);
};
const originalPermissionCheck = session.setPermissionCheckHandler;
if (typeof originalPermissionCheck === "function")
session.setPermissionCheckHandler = function blockedPermissionCheck() {
return originalPermissionCheck.call(this, (webContents, permission) => {
recordRuntime("permission", "permission-check", "blocked", {
target: identity(webContents, "webContents"),
argument_shapes: [shape(permission)],
error: true,
});
return false;
});
};
const originalCertificate = session.setCertificateVerifyProc;
if (typeof originalCertificate === "function")
session.setCertificateVerifyProc = function blockedCertificate() {
return originalCertificate.call(this, (request, callback) => {
recordRuntime("permission", "certificate-error", "blocked", {
argument_shapes: [shape(request)],
error: true,
});
if (typeof callback === "function") callback(-2);
});
};
};
const patchWindowOpenHandler = (webContents, contentsId) => {
if (webContents === null || webContents === undefined) return;
const original = webContents.setWindowOpenHandler;
if (typeof original !== "function") return;
const createGuardedHandler =
(handler) =>
(...args) => {
try {
if (typeof handler === "function") handler(...args);
} catch {
recordRuntime("popup-attempt", "window-open-handler", "failed", {
target: contentsId,
argument_shapes: args.map((value) => shape(value)),
error: true,
});
return { action: "deny" };
}
recordRuntime("popup-attempt", "window-open-handler", "blocked", {
target: contentsId,
argument_shapes: args.map((value) => shape(value)),
error: true,
});
return { action: "deny" };
};
try {
original.call(webContents, createGuardedHandler(undefined));
} catch {
recordRuntime("popup-attempt", "window-open-handler-install", "failed", {
target: contentsId,
error: true,
});
}
webContents.setWindowOpenHandler = function blockedWindowOpenHandler(
handler,
) {
return original.call(this, createGuardedHandler(handler));
};
};
const patchSessionManager = (sessionModule) => {
if (sessionModule === null || sessionModule === undefined) return;
patchSession(sessionModule.defaultSession);
for (const method of ["fromPartition", "fromPath"]) {
const original = sessionModule[method];
if (typeof original !== "function") continue;
sessionModule[method] = function patchedSessionFactory(...args) {
const session = original.apply(this, args);
patchSession(session);
return session;
};
}
};
const patchApplicationEffects = (app) => {
if (app === null || app === undefined) return;
for (const method of [
"relaunch",
"setAsDefaultProtocolClient",
"setLoginItemSettings",
]) {
const original = app[method];
if (typeof original !== "function") continue;
app[method] = function blockedApplicationEffect(...args) {
recordRuntime(
method === "relaunch" ? "updater" : "protocol",
method,
"blocked",
{ argument_shapes: args.map((value) => shape(value)), error: true },
);
return method === "setAsDefaultProtocolClient" ? false : undefined;
};
}
};
const patchNativeAddonLoading = () => {
const original = process.dlopen;
if (typeof original !== "function") return;
process.dlopen = function patchedDlopen(module, filename, ...args) {
let digest = null;
try {
const fs = require("node:fs");
const crypto = require("node:crypto");
const stat = fs.statSync(filename);
if (stat.isFile() && stat.size <= 16 * 1024 * 1024)
digest = crypto
.createHash("sha256")
.update(fs.readFileSync(filename))
.digest("hex");
} catch {
digest = null;
}
recordRuntime("native-addon", "dlopen", "attempted", {
artifact_path: typeof filename === "string" ? filename : null,
artifact_sha256: digest,
argument_shapes: [
shape(module),
shape(filename),
...args.map((value) => shape(value)),
],
});
try {
const result = original.call(this, module, filename, ...args);
recordRuntime("native-addon", "dlopen", "completed", {
artifact_path: typeof filename === "string" ? filename : null,
artifact_sha256: digest,
});
return result;
} catch (cause) {
recordRuntime("native-addon", "dlopen", "failed", {
artifact_path: typeof filename === "string" ? filename : null,
artifact_sha256: digest,
error: true,
});
throw cause;
}
};
};
const patchUtilityProcess = (utilityProcess) => {
if (utilityProcess === null || utilityProcess === undefined) return;
const original = utilityProcess.fork;
if (typeof original !== "function") return;
utilityProcess.fork = function patchedFork(...args) {
recordRuntime("process-lifecycle", "utility-process-fork", "attempted", {
argument_shapes: args.map((value) => shape(value)),
process_type: "main",
});
const child = original.apply(this, args);
const childId = identity(child, "utility");
recordRuntime("process-lifecycle", "utility-process-fork", "completed", {
target: childId,
process_type: "main",
});
patchEmitter(child, (event, eventArgs) => {
if (event === "message") {
recordIpc("ipc-utility-to-main", null, eventArgs, {
direction: "utility-to-main",
sender: childId,
receiver: "main",
target: childId,
process_type: "main",
error: false,
});
return;
}
recordRuntime("process-lifecycle", `utility.${event}`, "observed", {
target: childId,
process_type: "utility",
argument_shapes: eventArgs.map((value) => shape(value)),
});
});
if (child && typeof child.postMessage === "function") {
const postMessage = child.postMessage.bind(child);
child.postMessage = (...messageArgs) => {
const recorded = recordIpc(
"utility-process-message",
null,
messageArgs,
{
direction: "main-to-utility",
sender: "main",
receiver: childId,
target: childId,
process_type: "main",
error: false,
},
);
try {
const result = postMessage(...messageArgs);
if (recorded !== null) recorded.result_shape = shape(result);
return result;
} catch (cause) {
if (recorded !== null) recorded.error = true;
throw cause;
}
};
}
return child;
};
};
const patchShell = (shell) => {
if (shell === null || shell === undefined) return;
for (const method of ["openExternal", "openPath"]) {
const original = shell[method];
if (typeof original !== "function") continue;
shell[method] = function blockedShellCall(...args) {
recordRuntime("shell-attempt", method, "blocked", {
argument_shapes: args.map((value) => shape(value)),
error: true,
});
return Promise.reject(new Error("External shell effects are blocked"));
};
}
};
const patchChildProcess = () => {
const childProcess = require("node:child_process");
for (const method of ["spawn", "fork", "exec", "execFile"]) {
const original = childProcess[method];
if (typeof original !== "function") continue;
childProcess[method] = function patchedChildProcess(...args) {
recordRuntime("process-lifecycle", `child.${method}`, "attempted", {
argument_shapes: args.map((value) => shape(value)),
});
const child = original.apply(this, args);
const childId = identity(child, "child");
recordRuntime("process-lifecycle", `child.${method}`, "completed", {
target: childId,
});
patchEmitter(child, (event, eventArgs) =>
recordRuntime("process-lifecycle", `child.${event}`, "observed", {
target: childId,
argument_shapes: eventArgs.map((value) => shape(value)),
}),
);
return child;
};
}
};
return {
patchApplicationEffects,
patchChildProcess,
patchContextBridge,
patchIpcRenderer,
patchNativeAddonLoading,
patchSession,
patchSessionManager,
patchShell,
patchWindowOpenHandler,
patchUtilityProcess,
};
};
module.exports = { createElectronActiveBoundaryPatches };
+469
View File
@@ -0,0 +1,469 @@
"use strict";
const {
createElectronActiveBoundaryPatches,
} = require("./electron-active-hook-boundaries.cjs");
const ipcEventKinds = new Set([
"main-handler-invocation",
"main-event-invocation",
"utility-process-fork",
"utility-process-message",
"ipc-main-to-renderer",
"ipc-utility-to-main",
"ipc-renderer-send",
"ipc-renderer-invoke",
"ipc-renderer-post-message",
]);
const events = [];
let observed = 0;
let observedIpc = 0;
let observedRuntime = 0;
let sequence = 0;
let correlationSequence = 0;
const shape = (value, depth = 0) => {
if (depth > 2 || value === null) return value === null ? "null" : "nested";
if (Array.isArray(value)) return "array";
switch (typeof value) {
case "string":
return "string";
case "number":
return "number";
case "boolean":
return "boolean";
case "undefined":
return "undefined";
case "function":
return "function";
case "object":
return "object";
default:
return "unknown";
}
};
const isAllowedNavigation = (value) => {
if (typeof value !== "string") return true;
try {
const parsed = new URL(value);
if (["file:", "data:", "about:"].includes(parsed.protocol)) return true;
return (
["http:", "https:"].includes(parsed.protocol) &&
["127.0.0.1", "localhost", "[::1]", "::1"].includes(parsed.hostname)
);
} catch {
return false;
}
};
const processType = () =>
typeof process.type === "string" && process.type.length > 0
? process.type
: "main";
const identity = (value, prefix) => {
if (value === null || value === undefined) return null;
const id = value.id;
const processId = value.pid;
return typeof id === "string" || typeof id === "number"
? `${prefix}:${String(id)}`
: typeof processId === "number"
? `${prefix}:pid:${String(processId)}`
: null;
};
const frameIdentity = (event) => {
const frame = event?.senderFrame ?? event?.frame;
const id = frame?.frameTreeNodeId ?? frame?.routingId ?? frame?.id;
return typeof id === "string" || typeof id === "number"
? `frame:${String(id)}`
: null;
};
const record = (event) => {
observed += 1;
if (ipcEventKinds.has(event.kind)) observedIpc += 1;
else observedRuntime += 1;
const raw = {
sequence: ++sequence,
correlation_id:
typeof event.correlation_id === "string"
? event.correlation_id
: `capture:${process.pid}:${++correlationSequence}`,
event: null,
phase: "observed",
channel: null,
direction: null,
sender: null,
receiver: null,
frame: null,
target: null,
argument_shapes: [],
result_shape: null,
process_type: processType(),
source: "electron-active-hook",
capture_method: "api-wrapper",
artifact_path: null,
artifact_sha256: null,
error: false,
...event,
};
const argumentShapes = Array.isArray(raw.argument_shapes)
? raw.argument_shapes
: [];
events.push({
...raw,
correlation_id: raw.correlation_id,
event: typeof raw.event === "string" ? raw.event : null,
channel: typeof raw.channel === "string" ? raw.channel : null,
sender: typeof raw.sender === "string" ? raw.sender : null,
receiver: typeof raw.receiver === "string" ? raw.receiver : null,
frame: typeof raw.frame === "string" ? raw.frame : null,
target: typeof raw.target === "string" ? raw.target : null,
argument_shapes: argumentShapes.map((value) =>
typeof value === "string" ? value : "unknown",
),
result_shape:
typeof raw.result_shape === "string" ? raw.result_shape : null,
process_type:
typeof raw.process_type === "string" ? raw.process_type : null,
artifact_path:
typeof raw.artifact_path === "string" ? raw.artifact_path : null,
artifact_sha256:
typeof raw.artifact_sha256 === "string" ? raw.artifact_sha256 : null,
});
return events.at(-1);
};
const recordRuntime = (kind, event, phase, details = {}) =>
record({ kind, event, phase, ...details });
const recordIpc = (kind, channel, args, details = {}) =>
(() => {
const values = Array.isArray(args) ? args : [];
return record({
kind,
channel: typeof channel === "string" ? channel : null,
argument_shapes: values.map((value) => shape(value)),
...details,
});
})();
const patchEmitter = (emitter, observeEvent) => {
if (emitter === null || emitter === undefined) return;
const original = emitter.emit;
if (typeof original !== "function") return;
if (emitter.__reaElectronActiveEmitPatched === true) return;
try {
emitter.__reaElectronActiveEmitPatched = true;
emitter.emit = function patchedEmit(event, ...args) {
if (typeof event === "string") observeEvent.call(this, event, args);
return original.call(this, event, ...args);
};
} catch {
globalThis.__reaElectronActiveHookError = true;
}
};
const recordInvocation = (kind, channel, event, args) =>
recordIpc(kind, channel, args, {
direction: "renderer-to-main",
sender: identity(event?.sender, "webContents"),
receiver: "main",
frame: frameIdentity(event),
process_type: "main",
error: false,
});
const patchIpcMain = (ipcMain) => {
if (ipcMain === null || ipcMain === undefined) return;
for (const [method, kind] of [
["handle", "main-handler-invocation"],
["on", "main-event-invocation"],
["once", "main-event-invocation"],
]) {
const original = ipcMain[method];
if (typeof original !== "function") continue;
ipcMain[method] = function patchedIpcMain(channel, listener) {
if (typeof listener !== "function")
return original.call(this, channel, listener);
const wrapped =
kind === "main-handler-invocation"
? async function wrappedHandler(event, ...args) {
const recorded = recordInvocation(kind, channel, event, args);
try {
const result = await listener.call(this, event, ...args);
if (recorded !== null) recorded.result_shape = shape(result);
return result;
} catch (cause) {
if (recorded !== null) recorded.error = true;
throw cause;
}
}
: function wrappedEventListener(event, ...args) {
const recorded = recordInvocation(kind, channel, event, args);
try {
return listener.call(this, event, ...args);
} catch (cause) {
if (recorded !== null) recorded.error = true;
throw cause;
}
};
return original.call(this, channel, wrapped);
};
}
};
const patchNavigationMethods = (webContents, contentsId) => {
for (const method of ["loadURL", "loadFile", "loadDataURL"]) {
const original = webContents[method];
if (typeof original !== "function") continue;
webContents[method] = function patchedNavigation(...args) {
recordRuntime("navigation", method, "attempted", {
target: contentsId,
argument_shapes: args.map((value) => shape(value)),
});
if (method === "loadURL" && !isAllowedNavigation(args[0])) {
recordRuntime("navigation", method, "blocked", {
target: contentsId,
error: true,
});
return Promise.reject(new Error("External navigation is blocked"));
}
try {
const result = original.apply(this, args);
if (result !== null && typeof result?.then === "function")
return result.then(
(value) => {
recordRuntime("navigation", method, "completed", {
target: contentsId,
});
return value;
},
(cause) => {
recordRuntime("navigation", method, "failed", {
target: contentsId,
error: true,
});
throw cause;
},
);
recordRuntime("navigation", method, "completed", {
target: contentsId,
});
return result;
} catch (cause) {
recordRuntime("navigation", method, "failed", {
target: contentsId,
error: true,
});
throw cause;
}
};
}
};
const patchWebContentsIpc = (webContents, contentsId) => {
const originalSend = webContents.send;
if (typeof originalSend === "function")
webContents.send = function patchedSend(channel, ...args) {
const recorded = recordIpc("ipc-main-to-renderer", channel, args, {
direction: "main-to-renderer",
sender: "main",
receiver: contentsId,
target: contentsId,
process_type: "main",
error: false,
});
try {
const result = originalSend.call(this, channel, ...args);
if (recorded !== null) recorded.result_shape = shape(result);
return result;
} catch (cause) {
if (recorded !== null) recorded.error = true;
throw cause;
}
};
const originalPostMessage = webContents.postMessage;
if (typeof originalPostMessage === "function")
webContents.postMessage = function patchedPostMessage(
channel,
message,
transfer,
) {
const recorded = recordIpc(
"ipc-main-to-renderer",
channel,
[message, transfer],
{
direction: "main-to-renderer",
sender: "main",
receiver: contentsId,
target: contentsId,
process_type: "main",
error: false,
},
);
try {
const result = originalPostMessage.call(
this,
channel,
message,
transfer,
);
if (recorded !== null) recorded.result_shape = shape(result);
return result;
} catch (cause) {
if (recorded !== null) recorded.error = true;
throw cause;
}
};
};
const patchWebContents = (webContents, windowId) => {
if (webContents === null || webContents === undefined) return;
const contentsId = identity(webContents, "webContents");
patchEmitter(webContents, (event, args) => {
const navigation = [
"did-start-loading",
"did-stop-loading",
"did-finish-load",
"did-fail-load",
"did-frame-finish-load",
"will-navigate",
"will-redirect",
].includes(event);
const kind = navigation
? "navigation"
: [
"console-message",
"render-process-gone",
"crashed",
"unresponsive",
"responsive",
].includes(event)
? "error"
: event === "did-create-window"
? "popup-attempt"
: "web-contents-lifecycle";
if (event === "will-navigate" || event === "will-redirect") {
if (!isAllowedNavigation(args[1])) {
const preventDefault = args[0]?.preventDefault;
if (typeof preventDefault === "function") preventDefault.call(args[0]);
recordRuntime("navigation", event, "blocked", {
target: contentsId,
sender: windowId,
argument_shapes: args.map((value) => shape(value)),
error: true,
});
return;
}
}
recordRuntime(kind, event, "observed", {
target: contentsId,
sender: windowId,
argument_shapes: args.map((value) => shape(value)),
error: kind === "error",
});
if (event === "did-create-window") {
const childWindow = args[0];
patchWebContents(
childWindow?.webContents,
identity(childWindow, "window"),
);
}
});
patchNavigationMethods(webContents, contentsId);
patchWebContentsIpc(webContents, contentsId);
boundaries.patchWindowOpenHandler(webContents, contentsId);
};
const patchBrowserWindow = (electron) => {
const original = electron.BrowserWindow;
if (typeof original !== "function") return;
electron.BrowserWindow = new Proxy(original, {
construct(target, args, newTarget) {
const window = Reflect.construct(target, args, newTarget);
const targetId = identity(window, "window");
recordRuntime("window-lifecycle", "created", "completed", {
target: targetId,
argument_shapes: args.map((value) => shape(value)),
});
const preload = args[0]?.webPreferences?.preload;
if (typeof preload === "string")
recordRuntime("preload", "configured", "completed", {
target: targetId,
artifact_path: preload,
});
patchEmitter(window, (event) =>
recordRuntime("window-lifecycle", event, "observed", {
target: targetId,
}),
);
patchWebContents(window.webContents, targetId);
return window;
},
});
};
const boundaries = createElectronActiveBoundaryPatches({
identity,
patchEmitter,
processType,
recordIpc,
recordRuntime,
shape,
});
try {
const electron = require("electron");
patchEmitter(electron.app, (event, args) =>
recordRuntime(
event === "open-url" || event === "second-instance"
? "protocol"
: "app-lifecycle",
event,
"observed",
{
target: "app",
argument_shapes: args.map((value) => shape(value)),
process_type: "main",
},
),
);
patchIpcMain(electron.ipcMain);
boundaries.patchIpcRenderer(electron.ipcRenderer);
boundaries.patchContextBridge(electron.contextBridge);
patchBrowserWindow(electron);
boundaries.patchUtilityProcess(electron.utilityProcess);
boundaries.patchShell(electron.shell);
boundaries.patchSessionManager(electron.session);
boundaries.patchApplicationEffects(electron.app);
boundaries.patchNativeAddonLoading();
boundaries.patchChildProcess();
process.once("uncaughtException", (cause, origin) => {
recordRuntime("process-lifecycle", "uncaught-exception", "observed", {
argument_shapes: [shape(cause), shape(origin)],
error: true,
});
throw cause;
});
process.once("unhandledRejection", (reason, promise) => {
recordRuntime("process-lifecycle", "unhandled-rejection", "observed", {
argument_shapes: [shape(reason), shape(promise)],
error: true,
});
throw reason;
});
} catch {
globalThis.__reaElectronActiveHookError = true;
}
globalThis.__reaElectronActiveSnapshot = () => ({
events: events.slice(),
observed,
observed_ipc: observedIpc,
observed_runtime: observedRuntime,
hook_error: globalThis.__reaElectronActiveHookError === true,
});
@@ -7,6 +7,21 @@ export async function startBrowserVerifierSite() {
let port = 0;
let sessionGeneration = 0;
const server = createServer((request, response) => {
if (request.url === "/screenshot-noise") {
response.setHeader("content-type", "text/html");
response.end(`<!doctype html><html><body style="margin:0"><canvas width="2048" height="1536"></canvas><script>
const context = document.querySelector('canvas').getContext('2d');
const image = context.createImageData(2048, 1536);
let state = 0x6d2b79f5;
for (let index = 0; index < image.data.length; index++) {
state ^= state << 13; state ^= state >>> 17; state ^= state << 5;
image.data[index] = index % 4 === 3 ? 255 : state & 255;
}
context.putImageData(image, 0, 0);
document.body.dataset.ready = 'true';
</script></body></html>`);
return;
}
if (request.url === "/app.js.map") {
response.setHeader("content-type", "application/source-map+json");
response.end(sourceMap());
+15 -4
View File
@@ -30,16 +30,27 @@ const { stdout } = await exec(
},
);
const verifierOutput = JSON.parse(stdout);
// Managed conformance claims are host-independent. Keep the checked-in
// commitment reproducible across macOS, Linux, and Windows rather than
// embedding the machine that happened to generate it.
const portableCompletionReport = {
...verifierOutput.completionReport,
environment: {
platform: "portable",
architecture: "portable",
runtime: "node",
runtime_version: "portable",
},
};
const { createCompletionLedgerArtifacts } = await import(
`../dist/domain/completionLedgerGeneration.js?${String(Date.now())}`
);
const skillDigest = await directoryDigest(
join(root, "skills/reverse-engineer-anything"),
);
const generated = createCompletionLedgerArtifacts(
verifierOutput.completionReport,
[{ skill_id: "reverse-engineer-anything", sha256: skillDigest }],
);
const generated = createCompletionLedgerArtifacts(portableCompletionReport, [
{ skill_id: "reverse-engineer-anything", sha256: skillDigest },
]);
if (!check) await mkdir(outputRoot, { recursive: true });
await Promise.all([
+205
View File
@@ -0,0 +1,205 @@
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { format } from "prettier";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { Ajv2020 } from "ajv/dist/2020.js";
import { McpServer } from "@modelcontextprotocol/server";
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managedWorkflowToolContracts.js";
import { ArtifactProvider } from "../dist/artifacts/ArtifactProvider.js";
import { ManagedStaticProvider } from "../dist/dotnet/ManagedStaticProvider.js";
import { NativeMacOSProvider } from "../dist/native/NativeMacOSProvider.js";
import { toolRegistrationOptions } from "../dist/server/toolRegistrationOptions.js";
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
const arguments_ = new Set(process.argv.slice(2));
for (const argument of arguments_)
if (argument !== "--check")
throw new Error(`Unknown MCP tool catalog option: ${argument}`);
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
// The checked-in catalog is a portable package artifact. Keep its canonical
// provider metadata independent of the machine running the generator.
const CATALOG_PLATFORM = "darwin";
const sessionToolNames = new Set([
...TOOL_CONTRACTS.filter(({ kind }) => kind === "session").map(
({ name }) => name,
),
...MANAGED_WORKFLOW_TOOL_CONTRACTS.map(({ name }) => name),
]);
const advertisedTools = await sdkToolCatalog();
const ajv = new Ajv2020({ strict: false, validateFormats: false });
for (const tool of advertisedTools.values()) {
for (const kind of ["inputSchema", "outputSchema"]) {
const schema = tool[kind];
if (schema !== undefined && !ajv.validateSchema(schema))
throw new Error(
`Invalid JSON Schema for ${tool.name}.${kind}: ${ajv.errorsText(ajv.errors)}`,
);
}
}
const catalog = TOOL_CONTRACTS.map((contract) => {
const advertised = advertisedTools.get(contract.name);
if (advertised === undefined)
throw new Error(`MCP SDK omitted registered tool ${contract.name}`);
return {
name: contract.name,
analysisOperation: [
"official-proxy",
"enhanced",
"native-provider",
"artifact-provider",
"managed-provider",
].includes(contract.kind)
? contract.name
: null,
title: advertised.title,
description: advertised.description,
kind: contract.kind,
requiresSession: sessionToolNames.has(contract.name),
inputSchema: advertised.inputSchema,
outputSchema: advertised.outputSchema,
annotations: advertised.annotations,
effects: contract.effects,
};
});
async function sdkToolCatalog() {
const server = new McpServer({ name: "rea-catalog-generator", version: "0" });
for (const contract of TOOL_CONTRACTS)
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async () => ({
content: [{ type: "text", text: "Catalog-only handler" }],
isError: true,
}),
);
const client = new Client({ name: "rea-catalog-generator", version: "0" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const { tools } = await client.listTools();
return new Map(tools.map((tool) => [tool.name, tool]));
} finally {
await Promise.allSettled([client.close(), server.close()]);
}
}
const auxiliaryProviders = [
new ArtifactProvider(false, false, CATALOG_PLATFORM),
new NativeMacOSProvider(undefined, CATALOG_PLATFORM),
new ManagedStaticProvider(),
].map((provider) => ({
identity: provider.identity(),
capabilities: provider.capabilities(),
}));
const canonicalizeJson = (value) => {
if (Array.isArray(value)) return value.map(canonicalizeJson);
if (value === null || typeof value !== "object") return value;
return Object.fromEntries(
Object.entries(value)
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
.map(([key, entry]) => [key, canonicalizeJson(entry)]),
);
};
const payloadJson = JSON.stringify(
canonicalizeJson({ catalog, auxiliaryProviders }),
);
const source = await format(
`import type { ToolAnnotations } from "@modelcontextprotocol/server";
import type {
CapabilityDescriptor,
ProviderIdentity,
} from "./application/AnalysisProvider.js";
import type { ToolKind } from "./contracts/toolContractTypes.js";
import type { ToolEffects } from "./contracts/toolEffects.js";
interface GeneratedTool {
readonly name: string;
readonly analysisOperation: CapabilityDescriptor["operation"] | null;
readonly title: string;
readonly description: string;
readonly kind: ToolKind;
readonly requiresSession: boolean;
readonly inputSchema: Readonly<Record<string, unknown>>;
readonly outputSchema: Readonly<Record<string, unknown>>;
readonly annotations: ToolAnnotations;
readonly effects: ToolEffects;
}
interface GeneratedProvider {
readonly identity: ProviderIdentity;
readonly capabilities: readonly CapabilityDescriptor[];
}
interface GeneratedPayload {
readonly catalog: readonly GeneratedTool[];
readonly auxiliaryProviders: readonly GeneratedProvider[];
}
const GENERATED_PAYLOAD_JSON = ${JSON.stringify(payloadJson)};
const generatedPayload: unknown = JSON.parse(GENERATED_PAYLOAD_JSON);
if (!isGeneratedPayload(generatedPayload))
throw new TypeError("Generated MCP catalog payload is invalid");
/** Generated from TOOL_CONTRACTS; do not edit. */
export const GENERATED_MCP_TOOL_CATALOG = generatedPayload.catalog;
/** Generated lightweight metadata for lazily loaded auxiliary providers. */
export const GENERATED_AUXILIARY_PROVIDERS = generatedPayload.auxiliaryProviders;
function isGeneratedPayload(value: unknown): value is GeneratedPayload {
return (
isRecord(value) &&
Array.isArray(value.catalog) &&
value.catalog.every(isGeneratedTool) &&
Array.isArray(value.auxiliaryProviders) &&
value.auxiliaryProviders.every(isGeneratedProvider)
);
}
function isGeneratedTool(value: unknown): value is GeneratedTool {
return (
isRecord(value) &&
typeof value.name === "string" &&
(typeof value.analysisOperation === "string" ||
value.analysisOperation === null) &&
typeof value.title === "string" &&
typeof value.description === "string" &&
typeof value.kind === "string" &&
typeof value.requiresSession === "boolean" &&
isRecord(value.inputSchema) &&
isRecord(value.outputSchema) &&
isRecord(value.annotations) &&
isRecord(value.effects)
);
}
function isGeneratedProvider(value: unknown): value is GeneratedProvider {
return (
isRecord(value) &&
isRecord(value.identity) &&
Array.isArray(value.capabilities)
);
}
function isRecord(
value: unknown,
): value is Readonly<Record<string, unknown>> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
`,
{ parser: "typescript" },
);
await ensureGeneratedFile({
path: join(root, "src/generatedMcpToolCatalog.ts"),
source,
check: arguments_.has("--check"),
generateCommand: "npm run mcp-catalog:generate",
});
+13 -32
View File
@@ -36,11 +36,8 @@ UNEXPECTED_DISPLAY_GEOMETRY = 77
X11_INPUT_FAILED = 78
RUNTIME_DEPENDENCY_UNAVAILABLE = 79
X11_SOCKET_DIRECTORY_UNUSABLE = 80
DIAGNOSTIC_PREFIX = "REA_X11_DIAGNOSTIC_V1="
DIAGNOSTIC_PREFIX = "REA_X11_DIAGNOSTIC="
X11_SOCKET_DIRECTORY = Path("/tmp/.X11-unix")
XVFB_STDERR_LIMIT = 16 * 1024
class AdapterFailure(Exception):
def __init__(
self,
@@ -48,20 +45,18 @@ class AdapterFailure(Exception):
reason: str,
message: str,
stderr_bytes: int = 0,
stderr_truncated: bool = False,
) -> None:
super().__init__(message)
self.code = code
self.reason = reason
self.stderr_bytes = stderr_bytes
self.stderr_truncated = stderr_truncated
def with_stderr(self, capture: BoundedStderr) -> AdapterFailure:
self.stderr_bytes, self.stderr_truncated, _ = capture.snapshot()
def with_stderr(self, capture: CapturedStderr) -> AdapterFailure:
self.stderr_bytes, _ = capture.snapshot()
return self
class BoundedStderr:
class CapturedStderr:
def __init__(self, stream: object) -> None:
self._stream = stream
self._retained = bytearray()
@@ -77,15 +72,13 @@ class BoundedStderr:
return
with self._lock:
self._bytes += len(chunk)
remaining = XVFB_STDERR_LIMIT - len(self._retained)
if remaining > 0:
self._retained.extend(chunk[:remaining])
self._retained.extend(chunk)
def snapshot(self) -> tuple[int, bool, str]:
def snapshot(self) -> tuple[int, str]:
with self._lock:
retained = bytes(self._retained)
total = self._bytes
return total, total > len(retained), retained.decode("utf-8", "replace")
return total, retained.decode("utf-8", "replace")
def close(self) -> None:
self._thread.join(timeout=0.5)
@@ -284,14 +277,11 @@ def diagnostic(
operation: str,
status: str,
failure: AdapterFailure | None = None,
capture: BoundedStderr | None = None,
capture: CapturedStderr | None = None,
) -> dict[str, object]:
mode, read_only, _ = socket_directory_state()
captured_bytes, captured_truncated, _ = (
(0, False, "") if capture is None else capture.snapshot()
)
captured_bytes, _ = (0, "") if capture is None else capture.snapshot()
return {
"schema_version": 1,
"component": "hopper_private_display",
"operation": operation,
"status": status,
@@ -308,9 +298,6 @@ def diagnostic(
"xvfb_stderr_bytes": captured_bytes
if failure is None
else failure.stderr_bytes,
"xvfb_stderr_truncated": captured_truncated
if failure is None
else failure.stderr_truncated,
}
@@ -441,8 +428,8 @@ def authorize_display(
) from error
def classify_xvfb_failure(capture: BoundedStderr) -> AdapterFailure:
stderr_bytes, stderr_truncated, text = capture.snapshot()
def classify_xvfb_failure(capture: CapturedStderr) -> AdapterFailure:
stderr_bytes, text = capture.snapshot()
mode, read_only, directory = socket_directory_state()
if read_only is True:
return AdapterFailure(
@@ -450,7 +437,6 @@ def classify_xvfb_failure(capture: BoundedStderr) -> AdapterFailure:
"socket_directory_read_only",
"X11 socket directory is read-only",
stderr_bytes,
stderr_truncated,
)
if directory and mode != "1777":
return AdapterFailure(
@@ -458,7 +444,6 @@ def classify_xvfb_failure(capture: BoundedStderr) -> AdapterFailure:
"socket_directory_mode",
"X11 socket directory mode is not 1777",
stderr_bytes,
stderr_truncated,
)
if mode is not None and not directory:
return AdapterFailure(
@@ -466,7 +451,6 @@ def classify_xvfb_failure(capture: BoundedStderr) -> AdapterFailure:
"socket_directory_type",
"X11 socket path is not a directory",
stderr_bytes,
stderr_truncated,
)
lowered = text.lower()
if "error while loading shared libraries" in lowered:
@@ -475,7 +459,6 @@ def classify_xvfb_failure(capture: BoundedStderr) -> AdapterFailure:
"missing_library",
"Xvfb shared library unavailable",
stderr_bytes,
stderr_truncated,
)
if any(
marker in lowered
@@ -490,21 +473,19 @@ def classify_xvfb_failure(capture: BoundedStderr) -> AdapterFailure:
"address_collision",
"private display address collision",
stderr_bytes,
stderr_truncated,
)
return AdapterFailure(
PRIVATE_DISPLAY_UNAVAILABLE,
"xvfb_startup_failed",
"private X display unavailable",
stderr_bytes,
stderr_truncated,
)
def start_xvfb(
session: Path,
args: argparse.Namespace,
) -> tuple[subprocess.Popen[bytes], str, Path, BoundedStderr]:
) -> tuple[subprocess.Popen[bytes], str, Path, CapturedStderr]:
authority = session / "Xauthority"
collision: AdapterFailure | None = None
for _ in range(3):
@@ -551,7 +532,7 @@ def start_xvfb(
"xvfb_startup_failed",
"Xvfb stderr unavailable",
)
capture = BoundedStderr(process.stderr)
capture = CapturedStderr(process.stderr)
deadline = time.monotonic() + 2
socket = Path(f"/tmp/.X11-unix/X{display_number}")
while time.monotonic() < deadline and process.poll() is None:
+64
View File
@@ -0,0 +1,64 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
const environment = () => ({
PATH: process.env.PATH ?? "/usr/bin:/bin",
REA_LOG_LEVEL: "silent",
REA_ANALYSIS_PROVIDER: "auto",
// Artifact workflows have no deep-provider prerequisite or GUI authority.
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
});
/** Execute the compiled CLI with production providers and no test adapters. */
export async function artifactCli(command, target, arguments_ = []) {
const { stdout } = await promisify(execFile)(
process.execPath,
[
fileURLToPath(new URL("../rea.mjs", import.meta.url)),
command,
target,
...arguments_,
"--json",
],
{ env: environment(), timeout: 60000, maxBuffer: 16 * 1024 * 1024 },
);
const evidence = JSON.parse(stdout);
assert.equal(evidence.error, undefined, stdout);
assert.ok(evidence.normalized_result, "CLI omitted normalized result");
return evidence.normalized_result;
}
/** Run a real stdio MCP subprocess, owning and closing its transport. */
export async function withArtifactMcp(target, verify) {
const transport = new StdioClientTransport({
command: process.execPath,
args: [fileURLToPath(new URL("../rea.mjs", import.meta.url)), "mcp"],
env: environment(),
stderr: "pipe",
});
const client = new Client({ name: "artifact-real-e2e", version: "1" });
try {
await client.connect(transport);
const opened = await client.callTool({
name: "open_binary",
arguments: { path: target },
});
assert.notEqual(opened.isError, true, JSON.stringify(opened));
await verify(client);
} finally {
await client.close();
await transport.close();
}
}
/** Require a successful structured MCP tool result rather than its text rendering. */
export async function artifactMcpResult(client, name, arguments_ = {}) {
const result = await client.callTool({ name, arguments: arguments_ });
assert.notEqual(result.isError, true, JSON.stringify(result));
assert.ok(result.structuredContent?.result, "MCP omitted structured result");
return result.structuredContent.result;
}
-18
View File
@@ -42,7 +42,6 @@ const duplicateOriginStorage = (origin) => ({
/** Build the source-owned full-capture browser scenario. */
export function browserScenario(browser, origin) {
return browserScenarioSchema.parse({
schema_version: 1,
browser,
start_url: { url: `${origin}/app`, query: [] },
allowed_origins: [origin],
@@ -112,23 +111,6 @@ export function browserScenario(browser, origin) {
"downloads",
],
},
limits: {
max_duration_ms: 30_000,
action_timeout_ms: 5_000,
navigation_timeout_ms: 10_000,
max_events: 10_000,
max_frames: 100,
max_workers: 20,
max_popups: 10,
max_websockets: 100,
max_dom_nodes: 10_000,
max_accessibility_nodes: 10_000,
max_screenshots: 8,
max_screenshot_bytes: 4 * 1_024 * 1_024,
max_storage_entries: 256,
max_total_metadata_bytes: 8 * 1_024 * 1_024,
},
approved: true,
});
}
+115
View File
@@ -0,0 +1,115 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { chromium } from "playwright-core";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseEvidence } from "../../dist/domain/evidence.js";
import { webScreenshotSchema } from "../../dist/domain/webScreenshot.js";
import { comparePngScreenshots } from "../../dist/browser/PngVisualDiff.js";
/** Verify a real, large PNG through production CLI and stdio MCP boundaries. */
export async function verifyLargeScreenshotE2e(endpoint, origin) {
const browser = await chromium.connectOverCDP(endpoint);
const context = await browser.newContext({
viewport: { width: 2048, height: 1536 },
deviceScaleFactor: 1,
});
const entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
const env = {
PATH: process.env.PATH ?? "/usr/bin:/bin",
REA_LOG_LEVEL: "silent",
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
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;
try {
const page = await context.newPage();
await page.goto(`${origin}/screenshot-noise`);
await page.waitForFunction(() => document.body.dataset.ready === "true");
const targets = await (await fetch(`${endpoint}/json/list`)).json();
const target = targets.find((value) => value.url === page.url());
assert.ok(
target,
"Owned screenshot page is absent from actual CDP discovery",
);
const { stdout } = await promisify(execFile)(
process.execPath,
[entrypoint, "capture-web-screenshot", endpoint, target.id, "--json"],
{ env, timeout: 60000, maxBuffer: 72 * 1024 * 1024 },
);
const cliEvidence = parseEvidence(JSON.parse(stdout));
assert.equal(cliEvidence.operation, "capture_web_screenshot");
const screenshot = webScreenshotSchema.parse(cliEvidence.normalized_result);
assert.ok(
screenshot.artifact.bytes > 8 * 1024 * 1024,
"Real PNG did not cross the former screenshot byte ceiling",
);
const bytes = Buffer.from(screenshot.artifact.data_base64, "base64");
assert.equal(bytes.byteLength, screenshot.artifact.bytes);
assert.deepEqual(
bytes.subarray(0, 8),
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
);
assert.equal(bytes.readUInt32BE(16), 2048);
assert.equal(bytes.readUInt32BE(20), 1536);
transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
// The SDK defaults to a 10 MiB receive buffer; inline PNG output is larger.
maxBufferSize: 64 * 1024 * 1024,
});
client = new Client({ name: "browser-screenshot-real-e2e", version: "1" });
await client.connect(transport);
const captured = await client.callTool(
{
name: "capture_web_screenshot",
arguments: {
cdp_endpoint: endpoint,
allowed_origins: [origin],
target_id: target.id,
},
},
{ timeout: 60000 },
);
assert.notEqual(captured.isError, true, JSON.stringify(captured));
const mcpEvidence = parseEvidence({
...captured.structuredContent?.evidence,
normalized_result: captured.structuredContent?.result,
});
const mcpScreenshot = webScreenshotSchema.parse(
mcpEvidence.normalized_result,
);
assert.deepEqual(mcpScreenshot.artifact, screenshot.artifact);
const compared = comparePngScreenshots({
before: screenshot.artifact,
after: mcpScreenshot.artifact,
channel_threshold: 0,
});
assert.equal(compared.status, "identical");
assert.equal(compared.changed_pixels, 0);
return {
mocked: false,
cli: true,
stdio_mcp: true,
png_bytes: bytes.byteLength,
};
} finally {
try {
if (client !== undefined) await client.close();
} finally {
try {
if (transport !== undefined) await transport.close();
} finally {
await context.close();
await browser.close();
}
}
}
}
+47 -20
View File
@@ -81,26 +81,12 @@ export const toolFamilyCatalog = (sources) => {
/** Build provider identities with their sorted capability names. */
export const providerCatalog = (sources) => {
const applicationContracts =
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
({ name }) => name === "analyze_javascript_application",
);
const reconciliationContracts =
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
({ name }) => name === "reconcile_javascript_runtime",
);
const observationContracts =
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
({ name }) =>
name !== "analyze_javascript_application" &&
name !== "reconcile_javascript_runtime",
);
if (
applicationContracts.length !== 1 ||
reconciliationContracts.length !== 1 ||
observationContracts.length !== 2
)
throw new Error("Electron provider capability ownership drifted");
const {
applicationContracts,
reconciliationContracts,
observationContracts,
activeContracts,
} = electronContractSlices(sources);
return [
{
identity: sources.hopperProvider.HOPPER_PROVIDER_IDENTITY,
@@ -142,6 +128,12 @@ export const providerCatalog = (sources) => {
identity: sources.electronProvider.CDP_ELECTRON_PROVIDER_IDENTITY,
contracts: observationContracts,
},
{
identity:
sources.electronActiveProvider
.PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY,
contracts: activeContracts,
},
{
identity: sources.v8InspectorProvider.V8_INSPECTOR_PROVIDER_IDENTITY,
contracts:
@@ -171,3 +163,38 @@ export const providerCatalog = (sources) => {
}))
.sort((left, right) => left.id.localeCompare(right.id));
};
const electronContractSlices = (sources) => {
const applicationContracts =
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
({ name }) => name === "analyze_javascript_application",
);
const reconciliationContracts =
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
({ name }) => name === "reconcile_javascript_runtime",
);
const observationContracts =
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
({ name }) =>
name !== "analyze_javascript_application" &&
name !== "reconcile_javascript_runtime" &&
name !== "capture_electron_scenario",
);
const activeContracts =
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
({ name }) => name === "capture_electron_scenario",
);
if (
applicationContracts.length !== 1 ||
reconciliationContracts.length !== 1 ||
observationContracts.length !== 2 ||
activeContracts.length !== 1
)
throw new Error("Electron provider capability ownership drifted");
return {
applicationContracts,
reconciliationContracts,
observationContracts,
activeContracts,
};
};
+13 -1
View File
@@ -1,6 +1,9 @@
import { createHash } from "node:crypto";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import canonicalize from "canonicalize";
const load = (root, path) => import(pathToFileURL(join(root, path)).href);
/** Sort tool/provider names alphabetically. */
@@ -9,6 +12,14 @@ export const names = (contracts) =>
.map(({ name }) => name)
.sort((left, right) => left.localeCompare(right));
/** Return the stable digest for a generated catalog projection. */
export const digest = (value) => {
const encoded = canonicalize(value);
if (encoded === undefined)
throw new TypeError("Catalog projection is not canonical JSON");
return createHash("sha256").update(encoded).digest("hex");
};
/** Throw when two name lists diverge. */
export const assertSameNames = (label, actual, expected) => {
const actualSet = new Set(actual);
@@ -44,6 +55,7 @@ const SOURCE_PATHS = {
browserProvider: "dist/browser/CdpBrowserProvider.js",
browserScenarioProvider: "dist/browser/PlaywrightBrowserScenarioProvider.js",
electronProvider: "dist/browser/CdpElectronProvider.js",
electronActiveProvider: "dist/browser/PlaywrightElectronActiveProvider.js",
v8InspectorProvider: "dist/browser/V8InspectorProvider.js",
evidence: "dist/domain/evidence.js",
evidenceBundle: "dist/domain/evidenceBundle.js",
@@ -52,13 +64,13 @@ const SOURCE_PATHS = {
processCapture: "dist/domain/processCapture.js",
analysisSnapshot: "dist/domain/analysisSnapshot.js",
artifactGraph: "dist/domain/artifactGraph.js",
investigationWorkspace: "dist/domain/investigationWorkspace.js",
browserObservation: "dist/domain/browserObservation.js",
browserScenario: "dist/domain/browserScenario.js",
browserScenarioCapture: "dist/domain/browserScenarioCapture.js",
browserScenarioDiff: "dist/domain/browserScenarioDiff.js",
browserSession: "dist/domain/browserSession.js",
electronObservation: "dist/domain/electronObservation.js",
electronActiveObservation: "dist/domain/electronActiveObservation.js",
javascriptRuntimeObservation: "dist/domain/javascriptRuntimeObservation.js",
webBundleAnalysis: "dist/domain/webBundleAnalysis.js",
webCaptureDiff: "dist/domain/webCaptureDiff.js",
-262
View File
@@ -1,262 +0,0 @@
import { z } from "zod";
const valuesAt = (node, path) => {
if (typeof node !== "object" || node === null) return [];
if ("anyOf" in node && Array.isArray(node.anyOf))
return node.anyOf.flatMap((alternative) => valuesAt(alternative, path));
if (path.length === 0)
return "const" in node &&
(typeof node.const === "number" || typeof node.const === "string")
? [node.const]
: [];
if (
!("properties" in node) ||
typeof node.properties !== "object" ||
node.properties === null
)
return [];
const [segment, ...remaining] = path;
return segment !== undefined && segment in node.properties
? valuesAt(node.properties[segment], remaining)
: [];
};
const versionAt = (schema, path) => {
const versions = [
...new Set(
valuesAt(z.toJSONSchema(schema, { unrepresentable: "any" }), path).map(
String,
),
),
];
if (versions.length !== 1)
throw new Error(
`Schema version path ${path.join(".")} has ${String(versions.length)} constant values`,
);
const version = versions[0];
if (version === undefined) throw new Error("Schema version disappeared");
return /^\d+$/u.test(version) ? Number(version) : version;
};
const durableSchemaDefinitions = (sources) => [
[
"analysis_snapshot",
sources.analysisSnapshot.analysisSnapshotSchema,
["snapshot_version"],
],
[
"artifact_graph",
sources.artifactGraph.artifactInventoryResultSchema,
["manifest", "schema_version"],
],
[
"artifact_extraction",
sources.artifactGraph.artifactExtractionResultSchema,
["extraction_manifest", "schema_version"],
],
[
"browser_scenario",
sources.browserScenario.browserScenarioSchema,
["schema_version"],
],
["evidence", sources.evidence.evidenceSchema, ["schema_version"]],
[
"evidence_bundle",
sources.evidenceBundle.evidenceBundleSchema,
["bundle_version"],
],
[
"evidence_completion_ledger",
sources.evidenceCompletion.evidenceCompletionLedgerSchema,
["schema_version"],
],
[
"evidence_completion_manifest",
sources.completionGeneration.completionManifestSchema,
["schema_version"],
],
[
"investigation_run",
sources.investigationWorkspace.investigationRunSchema,
["schema_version"],
],
[
"investigation_run_summary",
sources.investigationWorkspace.investigationRunSummarySchema,
["schema_version"],
],
[
"investigation_workspace",
sources.investigationWorkspace.investigationWorkspaceSchema,
["workspace_version"],
],
[
"javascript_application_graph",
sources.javascriptApplicationGraph.javascriptApplicationGraphSchema,
["schema_version"],
],
[
"javascript_application_version_comparison",
sources.javascriptVersionComparison
.applicationVersionComparisonResultSchema,
["schema_version"],
],
[
"javascript_semantic_graph",
sources.javascriptSemanticGraph.javaScriptSemanticGraphSchema,
["schema_version"],
],
[
"javascript_semantic_query",
sources.javascriptSemanticQuery.javaScriptSemanticQueryResultSchema,
["schema_version"],
],
[
"javascript_semantic_trace",
sources.javascriptSemanticTrace.javaScriptSemanticTraceResultSchema,
["schema_version"],
],
[
"javascript_feature_trace",
sources.javascriptFeatureTrace.applicationFeatureTraceResultSchema,
["schema_version"],
],
[
"process_capture",
sources.processCapture.processCaptureSchema,
["schema_version"],
],
[
"reconstruction_verification",
sources.reconstructionVerification.reconstructionSpecificationSchema,
["schema_version"],
],
[
"residual_unknown",
sources.residualUnknown.residualUnknownSchema,
["registry_version"],
],
];
const browserScenarioObservationSchemaDefinitions = (sources) => [
[
"browser_scenario_capture",
sources.browserScenarioCapture.browserScenarioCaptureSchema,
["schema_version"],
],
[
"browser_scenario_diff",
sources.browserScenarioDiff.browserScenarioDiffSchema,
["schema_version"],
],
];
const observationSchemaDefinitions = (sources) => [
[
"managed_artifact_inspection",
sources.managedArtifact.managedArtifactInspectionSchema,
["schema_version"],
],
[
"managed_member_inspection",
sources.managedArtifact.managedMemberInspectionSchema,
["schema_version"],
],
[
"managed_native_boundary_inspection",
sources.managedArtifact.managedNativeBoundaryInspectionSchema,
["schema_version"],
],
[
"managed_member_comparison",
sources.managedComparison.managedMemberComparisonResultSchema,
["schema_version"],
],
[
"managed_native_verification",
sources.managedNativeVerification.managedNativeVerificationResultSchema,
["schema_version"],
],
[
"browser_target_list",
sources.browserObservation.browserTargetListSchema,
["schema_version"],
],
...browserScenarioObservationSchemaDefinitions(sources),
[
"electron_page_inspection",
sources.electronObservation.electronPageInspectionSchema,
["schema_version"],
],
[
"electron_target_list",
sources.electronObservation.electronTargetListSchema,
["schema_version"],
],
[
"javascript_application_analysis",
sources.javascriptApplicationAnalysis
.javaScriptApplicationAnalysisResultV2Schema,
["schema_version"],
],
[
"javascript_runtime_observation",
sources.javascriptRuntimeObservation.javascriptRuntimeObservationSchema,
["schema_version"],
],
[
"javascript_runtime_target_list",
sources.javascriptRuntimeObservation.javascriptRuntimeTargetListSchema,
["schema_version"],
],
[
"web_bundle_analysis",
sources.webBundleAnalysis.webBundleAnalysisSchema,
["schema_version"],
],
[
"web_capture_diff",
sources.webCaptureDiff.webCaptureDiffSchema,
["schema_version"],
],
[
"web_mcp_discovery",
sources.webMcpDiscovery.webMcpDiscoverySchema,
["schema_version"],
],
[
"web_observation_session",
sources.browserSession.webObservationSessionSchema,
["schema_version"],
],
[
"web_page_inspection",
sources.browserObservation.webPageInspectionSchema,
["schema_version"],
],
[
"web_screenshot",
sources.webScreenshot.webScreenshotSchema,
["schema_version"],
],
[
"web_screenshot_diff",
sources.webScreenshot.webScreenshotDiffSchema,
["schema_version"],
],
];
/** Derive the catalog of durable and observation schema versions. */
export const schemaCatalog = (sources) =>
[
...durableSchemaDefinitions(sources),
...observationSchemaDefinitions(sources),
]
.map(([id, schema, path]) => {
try {
return { id, version: versionAt(schema, path) };
} catch (cause) {
throw new Error(`Could not derive ${id} schema version`, { cause });
}
})
.sort((left, right) => left.id.localeCompare(right.id));
+1 -1
View File
@@ -9,7 +9,7 @@ export const HOPPER_C_ORACLE: Readonly<{
globalName: "rea_c_global";
}>;
export function sha256(value: string | Uint8Array): string;
export function generateLargeFixture(count?: number): string;
export function generateLargeFixture(): string;
export function sourceDigest(
sources: readonly { readonly path: string; readonly content: string }[],
): string;
+4 -6
View File
@@ -15,18 +15,16 @@ export const HOPPER_C_ORACLE = Object.freeze({
export const sha256 = (value) =>
createHash("sha256").update(value).digest("hex");
export const generateLargeFixture = (count = LARGE_FIXTURE_COUNT) => {
if (!Number.isInteger(count) || count < 1)
throw new Error("Fixture count must be a positive integer");
export const generateLargeFixture = () => {
const declarations = Array.from(
{ length: count },
{ length: LARGE_FIXTURE_COUNT },
(_, index) =>
`__attribute__((noinline, used)) int rea_page_${String(index).padStart(4, "0")}(void) { puts("REA_PAGE_${String(index).padStart(4, "0")}"); return ${index}; }`,
`__attribute__((noinline, used)) int rea_inventory_${String(index).padStart(4, "0")}(void) { puts("REA_INVENTORY_${String(index).padStart(4, "0")}"); return ${index}; }`,
);
return [
"#include <stdio.h>",
...declarations,
"int main(void) { return rea_page_0000(); }",
"int main(void) { return rea_inventory_0000(); }",
"",
].join("\n");
};
-17
View File
@@ -56,23 +56,6 @@ export const documentationFactIssues = async (root, catalog) => {
requireText(issues, templatePath, template, "docs/product-catalog.json");
const english = await readFile(join(root, "README.md"), "utf8");
const schemaVersion = (id) => {
const schema = catalog.schemas.find((candidate) => candidate.id === id);
if (schema === undefined) throw new Error(`Missing ${id} catalog schema`);
return String(schema.version);
};
requireText(
issues,
"README.md",
english,
`Evidence v${schemaVersion("evidence")}`,
);
requireText(
issues,
"README.md",
english,
`Process Capture v${schemaVersion("process_capture")}`,
);
requireText(issues, "README.md", english, "docs/product-catalog.json");
return issues;
};
+1 -4
View File
@@ -17,12 +17,11 @@ const completionClaim = ({
...new Set(targets.map(({ sha256: digest }) => digest)),
].sort();
const evidence = createEvidence(targets[0], COMPLETION_PROVIDER, {
predicateType: "rea.verification/v1",
predicateType: "rea.verification",
operation: "verify_managed_conformance",
parameters: {
scenario_id: scenarioId,
scenario_version: 1,
result_schema_version: 1,
artifact_sha256s: artifactSha256s,
},
result: { status },
@@ -37,7 +36,6 @@ const completionClaim = ({
id: COMPLETION_PROVIDER.id,
version: COMPLETION_PROVIDER.version,
},
result_schema_version: 1,
status,
evidence_ids: [evidence.evidence_id],
};
@@ -151,7 +149,6 @@ const optionalClaims = ({ operatorManifest, ilspyOracle }) => [
/** Project completed managed assertions into the standard verifier report. */
export const createManagedCompletionReport = (input, verifierRun) => ({
schema_version: 1,
verifier: {
id: COMPLETION_PROVIDER.id,
version: COMPLETION_PROVIDER.version,
+10 -32
View File
@@ -41,14 +41,6 @@ export const nativeBoundaryLimits = {
maxHeapItemBytes: 1024 * 1024,
};
export const applicationGraphLimits = {
max_types: 100,
max_methods: 100,
max_fields: 100,
max_pinvoke_imports: 100,
max_native_implementations: 100,
};
export const comparisonLimits = {
max_method_matches: 100,
max_field_matches: 100,
@@ -60,13 +52,6 @@ export const defaultIlBody = Buffer.from([
]);
export function functionDossier(name) {
const emptyPage = {
items: [],
total: 0,
returned: 0,
truncated: false,
next_offset: null,
};
return {
procedure: {
address: "0x401000",
@@ -80,23 +65,16 @@ export function functionDossier(name) {
signature: null,
locals: [],
},
pseudocode: {
text: "",
total_chars: 0,
returned_chars: 0,
truncated: false,
next_offset: null,
},
assembly: emptyPage,
comments: emptyPage,
callers: emptyPage,
callees: emptyPage,
incoming_references: emptyPage,
outgoing_references: emptyPage,
referenced_strings: emptyPage,
referenced_names: emptyPage,
basic_blocks: emptyPage,
instruction_scan: { scanned: 0, truncated: false },
pseudocode: "",
assembly: [],
comments: [],
callers: [],
callees: [],
incoming_references: [],
outgoing_references: [],
referenced_strings: [],
referenced_names: [],
basic_blocks: [],
limitations: [],
};
}
@@ -1,14 +1,9 @@
export function parseManagedAppManifest(rawManifest) {
const manifest = object(rawManifest, "manifest");
ensure(
manifest.schema_version === 1,
"manifest.schema_version must be exactly 1",
);
const target = object(manifest.target, "manifest.target");
const methods = array(manifest.methods, "manifest.methods");
ensure(manifest.methods.length > 0, "manifest.methods must not be empty");
return {
schema_version: 1,
label: optionalString(manifest.label, "manifest.label"),
target: {
path: string(target.path, "manifest.target.path"),
@@ -105,7 +100,7 @@ function parseManagedAppManifestMethod(rawMethod, index) {
method.signature_sha256,
`${prefix}.signature_sha256`,
),
il_size: ilSize(method, prefix),
il_size: nonNegativeInteger(method.il_size, `${prefix}.il_size`),
il_sha256:
method.il_sha256 === undefined
? undefined
@@ -117,26 +112,6 @@ function parseManagedAppManifestMethod(rawMethod, index) {
};
}
function ilSize(method, prefix) {
const hasIlSize = method.il_size !== undefined;
const hasIlLength = method.il_length !== undefined;
ensure(
hasIlSize || hasIlLength,
`${prefix}.il_size is required; il_length is accepted as a legacy alias`,
);
if (hasIlSize && hasIlLength)
ensure(
method.il_size === method.il_length,
`${prefix}.il_size and ${prefix}.il_length disagree`,
);
const value = hasIlSize ? method.il_size : method.il_length;
ensure(
Number.isInteger(value) && value >= 0,
`${prefix}.il_size must be a non-negative integer`,
);
return value;
}
function object(value, name) {
ensure(
value !== null && typeof value === "object" && !Array.isArray(value),
@@ -163,6 +138,14 @@ function optionalString(value, name) {
return string(value, name);
}
function nonNegativeInteger(value, name) {
ensure(
Number.isInteger(value) && value >= 0,
`${name} must be a non-negative integer`,
);
return value;
}
function digest(value, name) {
const text = string(value, name);
ensure(/^[a-f0-9]{64}$/u.test(text), `${name} must be a lowercase sha256`);
+4 -15
View File
@@ -1,7 +1,7 @@
import { basename, dirname, isAbsolute, resolve } from "node:path";
import { readFile } from "node:fs/promises";
import { parseBinaryTarget } from "../../dist/domain/binaryTarget.js";
import { parseBinaryTarget } from "../../dist/application/BinaryTargetResolver.js";
import { inspectManagedArtifactBytes } from "../../dist/dotnet/ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "../../dist/dotnet/ManagedMemberInspector.js";
import { projectManagedApplicationGraphEvidence } from "../../dist/application/ManagedApplicationGraphService.js";
@@ -202,13 +202,6 @@ function verifyManagedAppManifestApplicationGraph({
case_sensitive: expectedTrace.case_sensitive,
},
direction: "incoming",
limits: {
max_seed_matches: 10,
max_depth: 4,
max_nodes: 100,
max_edges: 200,
max_paths: 20,
},
});
ensure(
traced.ok,
@@ -245,11 +238,8 @@ function verifyManagedAppManifestApplicationGraph({
}
const projectManifestMethod = (state, token) => {
const {
appManifestInspectionLimits,
appManifestMemberLimits,
applicationGraphLimits,
} = state.context;
const { appManifestInspectionLimits, appManifestMemberLimits } =
state.context;
const existing = state.graphByToken.get(token);
if (existing !== undefined) return existing;
const method = state.inspectedMethods.get(token);
@@ -289,7 +279,6 @@ const projectManifestMethod = (state, token) => {
const projected = projectManagedApplicationGraphEvidence({
managed_artifact: artifactEvidence,
managed_members: memberEvidence,
limits: applicationGraphLimits,
});
ensure(
projected.ok,
@@ -316,7 +305,7 @@ function inspectManagedMethodByToken(context, bytes, target, expectedMethod) {
target,
expectedMethod.token,
);
const method = members.methods.items.find(
const method = members.methods.find(
({ token }) => token === expectedMethod.token,
);
ensure(
+2 -3
View File
@@ -39,11 +39,10 @@ async function runManagedAppManifestSelfTest(context) {
sample.target,
context.memberLimits,
);
const method = members.methods.items[0];
const method = members.methods[0];
assert.ok(method);
const summary = await context.verifyManagedAppManifest(
{
schema_version: 1,
label: "source-owned manifest verifier self-test",
target: {
path: sample.path,
@@ -143,7 +142,7 @@ async function runOptionalIlspyOracle(context) {
sample.target,
context.memberLimits,
);
const method = members.methods.items[0];
const method = members.methods[0];
ensureIlspy(method !== undefined, "source-owned fixture has no method");
const listOutput = await runIlspy(
ilspyPath,
+1 -1
View File
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { parseBinaryTarget } from "../../dist/domain/binaryTarget.js";
import { parseBinaryTarget } from "../../dist/application/BinaryTargetResolver.js";
import { buildManagedPeFixture } from "./managed-pe-fixture.mjs";
import { createManagedConformanceOracleSupport } from "./managed-conformance-oracles.mjs";
+62
View File
@@ -0,0 +1,62 @@
interface McpStartupPolicy {
readonly initializeBudgetMs: number;
readonly toolsListBudgetMs: number;
readonly firstCatalogBudgetMs: number;
readonly doctorDeadlineMs: number;
}
interface McpProcessOptions {
readonly command: string;
readonly args: readonly string[];
readonly environment: Readonly<Record<string, string | undefined>>;
readonly policy: McpStartupPolicy;
}
interface McpModuleProfileOptions extends McpProcessOptions {
readonly packageName: string;
}
interface RssMeasurement {
readonly supported: boolean;
readonly method: string | null;
readonly peak_bytes: number | null;
}
/** Measure the first usable MCP catalog through a child-process boundary. */
export function measureMcpStartup(options: McpProcessOptions): Promise<{
readonly process_started_to_initialized_ms: number;
readonly initialized_to_tools_list_ms: number;
readonly process_started_to_tools_list_ms: number;
readonly listed_tool_count: number;
readonly rss: RssMeasurement;
}>;
/** Profile ESM file loading in a separate diagnostic MCP child. */
export function profileMcpModuleLoading(
options: McpModuleProfileOptions,
): Promise<{
readonly supported: true;
readonly method: "node-debug-esm";
readonly unique_file_modules: number;
readonly unique_package_modules: number;
readonly unique_dependency_modules: number;
readonly debug_lines: number;
readonly truncated: boolean;
}>;
/** Parse Linux procfs VmRSS text into bytes. */
export function parseLinuxVmRssBytes(status: string): number | null;
/** Collect unique file-backed ESM module URLs from Node diagnostics. */
export function createEsmModuleProfileCollector(packageName?: string): {
append(chunk: string): void;
result(): {
readonly supported: true;
readonly method: "node-debug-esm";
readonly unique_file_modules: number;
readonly unique_package_modules: number;
readonly unique_dependency_modules: number;
readonly debug_lines: number;
readonly truncated: boolean;
};
};
+217
View File
@@ -0,0 +1,217 @@
import { readFileSync } from "node:fs";
import { performance } from "node:perf_hooks";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
const RSS_SAMPLE_INTERVAL_MS = 10;
const MAXIMUM_PROFILE_BUFFER_BYTES = 2 * 1_024 * 1_024;
const MAXIMUM_PROFILE_MODULES = 10_000;
/**
* Measure the first usable MCP catalog through the installed process boundary.
*/
export async function measureMcpStartup({
command,
args,
environment,
policy,
}) {
const transport = new StdioClientTransport({
command,
args,
env: environment,
stderr: "pipe",
});
let stderr = "";
transport.stderr?.on("data", (chunk) => {
stderr = boundedAppend(stderr, chunk.toString());
});
const rss = createLinuxRssSampler(() => transport.pid);
const client = new Client({
name: "package-startup-probe",
version: "1.0.0",
});
const startedAt = performance.now();
rss.start();
try {
await client.connect(transport, { timeout: policy.initializeBudgetMs });
const initializedAt = performance.now();
const tools = await client.listTools(undefined, {
timeout: policy.toolsListBudgetMs,
});
const listedAt = performance.now();
rss.stop();
const measurement = {
process_started_to_initialized_ms: elapsed(startedAt, initializedAt),
initialized_to_tools_list_ms: elapsed(initializedAt, listedAt),
process_started_to_tools_list_ms: elapsed(startedAt, listedAt),
listed_tool_count: tools.tools.length,
rss: rss.result(),
};
assertStartupBudgets(measurement, policy);
return measurement;
} catch (cause) {
throw new Error(`packaged MCP startup probe failed: ${stderr}`, { cause });
} finally {
rss.stop();
await Promise.allSettled([client.close(), transport.close()]);
}
}
/**
* Profile ESM file loading in a separate diagnostic child.
*/
export async function profileMcpModuleLoading({
command,
args,
environment,
policy,
packageName,
}) {
const profile = createEsmModuleProfileCollector(packageName);
const transport = new StdioClientTransport({
command,
args,
env: { ...environment, NODE_DEBUG: "esm" },
stderr: "pipe",
});
transport.stderr?.on("data", (chunk) => profile.append(chunk.toString()));
const client = new Client({
name: "package-module-profile",
version: "1.0.0",
});
try {
await client.connect(transport, {
timeout: policy.doctorDeadlineMs,
});
await client.listTools(undefined, {
timeout: policy.toolsListBudgetMs,
});
return profile.result();
} finally {
await Promise.allSettled([client.close(), transport.close()]);
}
}
/**
* Parse Linux procfs VmRSS text into bytes.
*/
export const parseLinuxVmRssBytes = (status) => {
const match = /^VmRSS:\s+(\d+)\s+kB$/mu.exec(status);
return match === null ? null : Number(match[1]) * 1_024;
};
/**
* Collect unique file-backed ESM module URLs from Node's diagnostic stream.
*/
export const createEsmModuleProfileCollector = (packageName = "rea-agents") => {
const modules = new Set();
let buffer = "";
let debugLines = 0;
let truncated = false;
const consume = (line) => {
if (line.startsWith("ESM ")) debugLines += 1;
for (const match of line.matchAll(/file:\/\/\/[^\s'",}\])]+/gu)) {
if (modules.size >= MAXIMUM_PROFILE_MODULES) {
truncated = true;
break;
}
modules.add(match[0]);
}
};
return {
append(chunk) {
buffer += chunk;
if (Buffer.byteLength(buffer) > MAXIMUM_PROFILE_BUFFER_BYTES) {
buffer = buffer.slice(-MAXIMUM_PROFILE_BUFFER_BYTES);
truncated = true;
}
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) consume(line);
},
result() {
if (buffer.length > 0) consume(buffer);
const values = [...modules];
const packageSegment = `/node_modules/${packageName}/`;
return {
supported: true,
method: "node-debug-esm",
unique_file_modules: values.length,
unique_package_modules: values.filter((value) =>
value.includes(packageSegment),
).length,
unique_dependency_modules: values.filter(
(value) =>
value.includes("/node_modules/") && !value.includes(packageSegment),
).length,
debug_lines: debugLines,
truncated,
};
},
};
};
const createLinuxRssSampler = (pid) => {
if (process.platform !== "linux")
return {
start() {},
stop() {},
result: () => ({
supported: false,
method: null,
peak_bytes: null,
}),
};
let peakBytes = null;
let timer;
const sample = () => {
const currentPid = pid();
if (currentPid === null) return;
try {
const current = parseLinuxVmRssBytes(
readFileSync(`/proc/${String(currentPid)}/status`, "utf8"),
);
if (current !== null && (peakBytes === null || current > peakBytes))
peakBytes = current;
} catch {
// The short-lived child may exit between resolving its PID and reading.
}
};
return {
start() {
sample();
timer = setInterval(sample, RSS_SAMPLE_INTERVAL_MS);
timer.unref();
},
stop() {
if (timer !== undefined) clearInterval(timer);
sample();
},
result: () => ({
supported: true,
method: "linux-proc-status-vmrss-sampled",
peak_bytes: peakBytes,
}),
};
};
const assertStartupBudgets = (measurement, policy) => {
if (measurement.process_started_to_initialized_ms > policy.initializeBudgetMs)
throw new Error("packaged MCP exceeded the initialize regression budget");
if (measurement.initialized_to_tools_list_ms > policy.toolsListBudgetMs)
throw new Error("packaged MCP exceeded the tools/list regression budget");
if (
measurement.process_started_to_tools_list_ms > policy.firstCatalogBudgetMs
)
throw new Error(
"packaged MCP exceeded the first-catalog regression budget",
);
};
const elapsed = (startedAt, completedAt) =>
Math.round((completedAt - startedAt) * 100) / 100;
const boundedAppend = (current, chunk) =>
`${current}${chunk}`.slice(-64 * 1_024);
+11 -49
View File
@@ -19,35 +19,29 @@ export const requireMcpResult = (result, operation) => {
return value.result;
};
/** Verify direct-provider provenance through the complete Evidence resource. */
export const requireEvidenceProvider = async (
client,
result,
operation,
providerId,
) => {
const evidence = await readMcpEvidence(client, result, operation);
/** Verify provider identity directly from the tool result. */
export const requireEvidenceProvider = (result, operation, providerId) => {
const evidence = jsonValue(result);
if (
evidence?.provider?.id !== providerId ||
evidence?.analysis_profile?.provider?.id !== providerId ||
typeof evidence.analysis_profile.provider.version !== "string"
evidence?.evidence?.provider?.id !== providerId ||
evidence?.evidence?.analysis_profile?.provider?.id !== providerId ||
typeof evidence.evidence.analysis_profile.provider.version !== "string"
) {
throw new Error(`${operation} omitted its concrete provider provenance`);
}
};
/** Verify composed-workflow and upstream provenance through full Evidence. */
export const requireWorkflowEvidenceProvider = async (
client,
/** Verify composed workflow and upstream provider provenance inline. */
export const requireWorkflowEvidenceProvider = (
result,
operation,
expected,
) => {
const evidence = await readMcpEvidence(client, result, operation);
const profile = evidence?.analysis_profile;
const evidence = jsonValue(result);
const profile = evidence?.evidence?.analysis_profile;
const upstream = profile?.parameters?.upstream_analysis_profile;
if (
evidence?.provider?.id !== expected.workflowProviderId ||
evidence?.evidence?.provider?.id !== expected.workflowProviderId ||
profile?.provider?.id !== expected.workflowProviderId ||
upstream?.provider?.id !== expected.upstreamProviderId ||
typeof upstream.provider.version !== "string"
@@ -57,35 +51,3 @@ export const requireWorkflowEvidenceProvider = async (
);
}
};
const readMcpEvidence = async (client, result, operation) => {
const compact = jsonValue(result);
if (
compact === null ||
typeof compact !== "object" ||
typeof compact.evidence_id !== "string" ||
compact.evidence_uri !== `rea://evidence/${compact.evidence_id}`
) {
throw new Error(`${operation} omitted its Evidence reference`);
}
const resource = await client.readResource({ uri: compact.evidence_uri });
const text = resource.contents.find(
(content) =>
content.uri === compact.evidence_uri && typeof content.text === "string",
)?.text;
if (text === undefined)
throw new Error(`${operation} Evidence resource omitted JSON text`);
const evidence = JSON.parse(text);
if (
evidence === null ||
typeof evidence !== "object" ||
evidence.evidence_id !== compact.evidence_id ||
JSON.stringify(evidence.normalized_result) !==
JSON.stringify(compact.result)
) {
throw new Error(
`${operation} Evidence resource did not match its projection`,
);
}
return evidence;
};
+183
View File
@@ -0,0 +1,183 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseEvidence } from "../../dist/domain/evidence.js";
import { analysisProfileSchema } from "../../dist/domain/analysisProfile.js";
import { nativeValueTraceSchema } from "../../dist/domain/nativeValueTrace.js";
const entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
const environment = () => ({
PATH: process.env.PATH ?? "/usr/bin:/bin",
REA_LOG_LEVEL: "silent",
REA_ANALYSIS_PROVIDER: "ghidra",
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
...(process.env.GHIDRA_INSTALL_DIR === undefined
? {}
: { GHIDRA_INSTALL_DIR: process.env.GHIDRA_INSTALL_DIR }),
...(process.env.JAVA_HOME === undefined
? {}
: { JAVA_HOME: process.env.JAVA_HOME }),
});
const verifyGraph = (input, target, procedure, globalAddress) => {
const graph = nativeValueTraceSchema.parse(input);
assert.equal(graph.target_sha256, target.sha256);
assert.equal(graph.seed, procedure);
const nodes = new Map(graph.nodes.map((node) => [node.id, node]));
for (const kind of ["argument-binding", "parameter-use", "return-binding"]) {
const edges = graph.edges.filter((edge) => edge.kind === kind);
assert.ok(edges.length > 0, `Real native dependency graph lacks ${kind}`);
for (const edge of edges) {
const source = nodes.get(edge.source);
const destination = nodes.get(edge.target);
assert.ok(source, `Missing source node ${edge.source}`);
assert.ok(destination, `Missing target node ${edge.target}`);
assert.equal(edge.status, "derived");
if (kind === "argument-binding") {
assert.match(source.operation?.opcode ?? "", /^CALL(?:IND)?$/u);
assert.equal(destination.kind, "parameter");
assert.equal(edge.input_index, destination.parameter.ordinal + 1);
} else if (kind === "parameter-use") {
assert.equal(source.kind, "parameter");
assert.ok(destination.operation.inputs[edge.input_index]);
} else {
assert.equal(source.operation?.opcode, "RETURN");
assert.match(destination.operation?.opcode ?? "", /^CALL(?:IND)?$/u);
assert.equal(edge.input_index, 1);
}
}
}
assert.ok(
graph.nodes.some((node) =>
node.operation?.inputs.some((value) => value.location === globalAddress),
),
"Source-owned global operand is absent from the real dependency graph",
);
for (const node of graph.nodes)
assert.match(node.evidence_id, /^ev_[a-f0-9]{64}$/u);
for (const procedure of graph.procedures) {
assert.equal(procedure.provider, "ghidra");
assert.match(procedure.analysis_profile_digest, /^[a-f0-9]{64}$/u);
}
return graph;
};
/** Verify real Ghidra dependency composition through production CLI and stdio MCP. */
export async function verifyNativeValueE2e(target, procedure, globalAddress) {
const { stdout } = await promisify(execFile)(
process.execPath,
[
entrypoint,
"trace-native-values",
target.path,
procedure,
"--provider",
"ghidra",
"--max-depth",
"2",
"--max-functions",
"8",
"--limit",
"5000",
"--json",
],
{ env: environment(), timeout: 240000, maxBuffer: 72 * 1024 * 1024 },
);
const evidence = parseEvidence(JSON.parse(stdout));
assert.equal(evidence.operation, "trace_native_values");
assert.equal(evidence.provider.id, "rea-workflow");
const workflowProfile = analysisProfileSchema.parse(
evidence.analysis_profile,
);
assert.equal(workflowProfile.provider.id, "rea-workflow");
const upstreamProfile = analysisProfileSchema.parse(
workflowProfile.parameters.upstream_analysis_profile,
);
assert.equal(upstreamProfile.provider.id, "ghidra");
const cliGraph = verifyGraph(
evidence.normalized_result,
target,
procedure,
globalAddress,
);
for (const procedure of cliGraph.procedures)
assert.equal(procedure.analysis_profile_digest, upstreamProfile.digest);
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env: environment(),
stderr: "pipe",
});
const client = new Client({ name: "native-values-real-e2e", version: "1" });
try {
await client.connect(transport);
const opened = await client.callTool(
{
name: "open_binary",
arguments: { path: target.path, provider: "ghidra" },
},
{ timeout: 180000 },
);
assert.notEqual(opened.isError, true, JSON.stringify(opened));
const inventory = await client.callTool({
name: "binary_session",
arguments: {},
});
assert.notEqual(inventory.isError, true, JSON.stringify(inventory));
assert.ok(
inventory.structuredContent?.result?.tool_availability.some(
(tool) => tool.name === "trace_native_values" && tool.available,
),
"Native value tracing is unavailable in real provider discovery",
);
const result = await client.callTool(
{
name: "trace_native_values",
arguments: { procedure, max_depth: 2, max_functions: 8, limit: 5000 },
},
{ timeout: 180000 },
);
assert.notEqual(result.isError, true, JSON.stringify(result));
const mcpEvidence = parseEvidence({
...result.structuredContent?.evidence,
normalized_result: result.structuredContent?.result,
});
assert.equal(mcpEvidence.operation, "trace_native_values");
assert.equal(mcpEvidence.provider.id, "rea-workflow");
assert.deepEqual(mcpEvidence.analysis_profile, workflowProfile);
const mcpGraph = verifyGraph(
result.structuredContent?.result,
target,
procedure,
globalAddress,
);
assert.deepEqual(mcpEvidence.normalized_result, mcpGraph);
assert.deepEqual(mcpGraph, cliGraph);
const closed = await client.callTool({
name: "close_binary",
arguments: {},
});
assert.notEqual(closed.isError, true, JSON.stringify(closed));
const status = await client.callTool({
name: "binary_session",
arguments: {},
});
assert.equal(status.structuredContent?.result?.open, false);
} finally {
await client.close();
await transport.close();
}
return {
mocked: false,
cli: true,
stdio_mcp: true,
nodes: cliGraph.total_nodes,
edges: cliGraph.total_edges,
decompilations: cliGraph.decompilations,
unknowns: cliGraph.unknowns.length,
};
}
+3 -1
View File
@@ -1,5 +1,4 @@
export interface ProductCatalog {
readonly catalog_schema_version: number;
readonly package: {
readonly name: string;
readonly version: string;
@@ -46,6 +45,9 @@ export interface ProductCatalog {
}
export function createProductCatalog(root: string): Promise<ProductCatalog>;
export function providerCatalogDigest(
providers: ProductCatalog["providers"],
): string;
export function createCliInventory(cli: unknown): {
readonly primary: readonly string[];
readonly aliases: readonly {
+10 -6
View File
@@ -1,13 +1,12 @@
import { format } from "prettier";
import { assertSameNames, loadSources } from "./catalog-core.mjs";
import { assertSameNames, digest, loadSources } from "./catalog-core.mjs";
import {
createCliInventory,
cliCommandDescriptionIssues,
cliCommandOptionNames,
} from "./catalog-cli.mjs";
import { providerCatalog, toolFamilyCatalog } from "./catalog-builders.mjs";
import { schemaCatalog } from "./catalog-schemas.mjs";
export {
createCliInventory,
@@ -15,6 +14,9 @@ export {
cliCommandOptionNames,
};
/** Return the stable digest for the source-derived provider projection. */
export const providerCatalogDigest = (providers) => digest(providers);
/** Project current runtime contracts into deterministic, machine-readable facts. */
export const createProductCatalog = async (root) => {
const sources = await loadSources(root);
@@ -25,9 +27,9 @@ export const createProductCatalog = async (root) => {
sources.catalogIdentity.CLI_COMMAND_NAMES,
);
const tools = toolFamilyCatalog(sources);
const providers = providerCatalog(sources);
const metadata = sources.packageMetadata.PACKAGE_METADATA;
return {
catalog_schema_version: 1,
package: {
name: metadata.name,
version: metadata.version,
@@ -39,7 +41,7 @@ export const createProductCatalog = async (root) => {
skill_version: metadata.skillVersion,
},
tools,
providers: providerCatalog(sources),
providers,
setup_clients: sources.supportedClients.SUPPORTED_CLIENT_DEFINITIONS.map(
({ name, displayName, format }) => ({
id: name,
@@ -48,7 +50,6 @@ export const createProductCatalog = async (root) => {
configuration: format === "unsupported" ? "detect-only" : "managed",
}),
),
schemas: schemaCatalog(sources),
cli: {
primary_count: cli.primary.length,
commands: cli.primary,
@@ -56,7 +57,10 @@ export const createProductCatalog = async (root) => {
},
runtime_catalog: {
counts: sources.catalogIdentity.CATALOG_IDENTITY.counts,
digests: sources.catalogIdentity.CATALOG_IDENTITY.digests,
digests: {
...sources.catalogIdentity.CATALOG_IDENTITY.digests,
providers_sha256: providerCatalogDigest(providers),
},
},
};
};
@@ -0,0 +1,21 @@
interface ToolClient {
callTool(request: unknown, options: unknown): Promise<unknown>;
}
interface SearchSummary {
readonly count: number;
readonly calls: 1;
}
export function openAndVerifyLargeFixture(input: {
readonly client: ToolClient;
readonly options: unknown;
readonly normalizedResult: (value: unknown, operation: string) => unknown;
readonly path: string;
readonly expectedCount: number;
readonly symbolPrefix: string;
readonly stringPrefix: string;
}): Promise<{
readonly procedures: SearchSummary;
readonly strings: SearchSummary;
}>;
@@ -0,0 +1,83 @@
/** Open the large fixture and verify complete inline search results. */
export async function openAndVerifyLargeFixture({
client,
options,
normalizedResult,
path,
expectedCount,
symbolPrefix,
stringPrefix,
}) {
const opened = await client.callTool(
{ name: "open_binary", arguments: { path } },
options,
);
if (opened.isError === true)
throw new Error("Hopper rejected the large fixture");
const procedures = await collectAllMatches({
client,
options,
normalizedResult,
tool: "search_procedures",
pattern: symbolPrefix.replace(/^_+/u, ""),
expectedPrefix: symbolPrefix.replace(/^_+/u, ""),
expectedCount,
normalize: (value) => value.replace(/^_+/u, ""),
});
const strings = await collectAllMatches({
client,
options,
normalizedResult,
tool: "search_strings",
pattern: stringPrefix,
expectedPrefix: stringPrefix,
expectedCount,
normalize: (value) => value,
});
return { procedures, strings };
}
async function collectAllMatches({
client,
options,
normalizedResult,
tool,
pattern,
expectedPrefix,
expectedCount,
normalize,
}) {
const matches = normalizedResult(
await client.callTool(
{
name: tool,
arguments: { pattern, mode: "literal", case_sensitive: true },
},
options,
),
tool,
);
if (!Array.isArray(matches) || matches.length !== expectedCount)
throw new Error(`${tool} did not return every expected match`);
const addresses = new Set();
const values = new Set();
for (const item of matches) {
const value = normalize(item.value);
if (
typeof item.address !== "string" ||
!/^0x[0-9a-f]+$/iu.test(item.address)
)
throw new Error(`${tool} returned a malformed address`);
if (addresses.has(item.address) || values.has(value))
throw new Error(`${tool} returned duplicate matches`);
addresses.add(item.address);
values.add(value);
}
const expectedValues = Array.from(
{ length: expectedCount },
(_, index) => `${expectedPrefix}${String(index).padStart(4, "0")}`,
);
if (expectedValues.some((value) => !values.has(value)))
throw new Error(`${tool} omitted expected matches`);
return { count: values.size, calls: 1 };
}
+2 -2
View File
@@ -6,7 +6,7 @@ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
export async function loadRealHopperFixtureTargets(manifestPath) {
const canonicalManifest = await realpath(manifestPath);
const manifest = JSON.parse(await readFile(canonicalManifest, "utf8"));
if (manifest?.schemaVersion !== 1 || !Array.isArray(manifest.fixtures))
if (!Array.isArray(manifest?.fixtures))
throw new Error("Invalid conformance fixture manifest");
const primary = fixtureEntry(manifest.fixtures, "c");
const secondary = fixtureEntry(manifest.fixtures, "version-v2");
@@ -69,7 +69,7 @@ function requireLargeOracle(largeOracle) {
largeOracle.symbolCount !== largeOracle.stringCount ||
largeOracle.symbolCount <= 0
)
throw new Error("Large fixture omitted its exact pagination oracle");
throw new Error("Large fixture omitted its complete-inventory oracle");
return largeOracle;
}
+8 -13
View File
@@ -31,23 +31,21 @@ export const verifyHopperFunctionBasics = async (
arguments: {
procedure,
direction: "outgoing",
limit: 10,
max_instructions: 100,
},
},
options,
),
"procedure_references",
);
if (!Array.isArray(references?.references?.items)) {
throw new Error("procedure_references returned an invalid bounded result");
if (!Array.isArray(references?.references)) {
throw new Error("procedure_references returned an invalid result");
}
const instructions = requireMcpResult(
await client.callTool(
{
name: "read_function_instructions",
arguments: { procedure, limit: 2 },
arguments: { procedure },
},
options,
),
@@ -55,17 +53,14 @@ export const verifyHopperFunctionBasics = async (
);
if (
instructions?.procedure?.address !== procedure ||
!Array.isArray(instructions.instructions?.items) ||
instructions.instructions.items.length === 0 ||
instructions.instructions.items.length > 2
!Array.isArray(instructions.instructions) ||
instructions.instructions.length === 0
) {
throw new Error(
"read_function_instructions returned an invalid bounded window",
);
throw new Error("read_function_instructions returned an invalid result");
}
return {
outgoingReferenceCount: references.references.items.length,
instructionWindowCount: instructions.instructions.items.length,
outgoingReferenceCount: references.references.length,
instructionWindowCount: instructions.instructions.length,
};
};
+47
View File
@@ -0,0 +1,47 @@
import assert from "node:assert/strict";
import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProvider.js";
import {
requireMcpResult,
requireEvidenceProvider,
} from "./mcp-verifier-results.mjs";
/** Verify real Hopper inventories and address filters through the MCP transport. */
export async function verifyHopperInventories(client, options) {
const inventoryCounts = {};
for (const operation of ["list_strings", "list_names"]) {
const called = await client.callTool(
{ name: operation, arguments: {} },
options,
);
const inventory = requireMcpResult(called, operation);
requireEvidenceProvider(called, operation, HOPPER_PROVIDER_IDENTITY.id);
assert.ok(
Array.isArray(inventory) && inventory.length > 0,
`${operation} omitted the real fixture inventory`,
);
let previous = -1n;
for (const item of inventory) {
assert.match(item.address, /^0x[0-9a-f]+$/u);
assert.equal(typeof item.value, "string");
const address = BigInt(item.address);
assert.ok(
address > previous,
`${operation} is not numerically address-sorted`,
);
previous = address;
}
const filtered = requireMcpResult(
await client.callTool(
{
name: operation,
arguments: { address: inventory[0].address },
},
options,
),
operation,
);
assert.deepEqual(filtered, [inventory[0]]);
inventoryCounts[operation] = inventory.length;
}
return inventoryCounts;
}
-33
View File
@@ -1,33 +0,0 @@
interface ToolClient {
callTool(request: unknown, options: unknown): Promise<unknown>;
}
interface PaginationSummary {
readonly count: number;
readonly pages: number;
}
export function verifyLargeFixturePagination(input: {
readonly client: ToolClient;
readonly options: unknown;
readonly normalizedResult: (value: unknown, operation: string) => unknown;
readonly expectedCount: number;
readonly symbolPrefix: string;
readonly stringPrefix: string;
}): Promise<{
readonly procedures: PaginationSummary;
readonly strings: PaginationSummary;
}>;
export function openAndVerifyLargeFixture(input: {
readonly client: ToolClient;
readonly options: unknown;
readonly normalizedResult: (value: unknown, operation: string) => unknown;
readonly path: string;
readonly expectedCount: number;
readonly symbolPrefix: string;
readonly stringPrefix: string;
}): Promise<{
readonly procedures: PaginationSummary;
readonly strings: PaginationSummary;
}>;
-138
View File
@@ -1,138 +0,0 @@
const PAGE_LIMIT = 100;
/** Open the large fixture and exhaustively prove its public search pages. */
export async function openAndVerifyLargeFixture({
client,
options,
normalizedResult,
path,
expectedCount,
symbolPrefix,
stringPrefix,
}) {
const opened = await client.callTool(
{ name: "open_binary", arguments: { path } },
options,
);
if (opened.isError === true)
throw new Error("Hopper rejected the large fixture");
return verifyLargeFixturePagination({
client,
options,
normalizedResult,
expectedCount,
symbolPrefix,
stringPrefix,
});
}
/** Exhaustively prove the source-owned large fixture through public search pages. */
export async function verifyLargeFixturePagination({
client,
options,
normalizedResult,
expectedCount,
symbolPrefix,
stringPrefix,
}) {
const procedures = await collectExactPages({
client,
options,
normalizedResult,
tool: "search_procedures",
pattern: symbolPrefix.replace(/^_+/u, ""),
expectedPrefix: symbolPrefix.replace(/^_+/u, ""),
expectedCount,
normalize: (value) => value.replace(/^_+/u, ""),
});
const strings = await collectExactPages({
client,
options,
normalizedResult,
tool: "search_strings",
pattern: stringPrefix,
expectedPrefix: stringPrefix,
expectedCount,
normalize: (value) => value,
});
return { procedures, strings };
}
async function collectExactPages({
client,
options,
normalizedResult,
tool,
pattern,
expectedPrefix,
expectedCount,
normalize,
}) {
const seenAddresses = new Set();
const seenValues = new Set();
let offset = 0;
let pages = 0;
while (true) {
const page = normalizedResult(
await client.callTool(
{
name: tool,
arguments: {
pattern,
mode: "literal",
case_sensitive: true,
offset,
limit: PAGE_LIMIT,
},
},
options,
),
`${tool} offset ${offset}`,
);
validatePage(page, { offset, expectedCount });
for (const item of page.items) {
const value = normalize(item.value);
if (item.value_truncated || !/^0x[0-9a-f]+$/iu.test(item.address))
throw new Error(`${tool} returned truncated or malformed evidence`);
if (seenAddresses.has(item.address) || seenValues.has(value))
throw new Error(`${tool} returned duplicate evidence`);
seenAddresses.add(item.address);
seenValues.add(value);
}
pages += 1;
if (page.next_offset === null) break;
offset = page.next_offset;
}
const expectedValues = Array.from(
{ length: expectedCount },
(_, index) => `${expectedPrefix}${String(index).padStart(4, "0")}`,
);
if (
seenValues.size !== expectedCount ||
expectedValues.some((value) => !seenValues.has(value))
)
throw new Error(`${tool} did not exhaustively prove the fixture values`);
return { count: seenValues.size, pages };
}
function validatePage(page, { offset, expectedCount }) {
if (
page === null ||
typeof page !== "object" ||
!Array.isArray(page.items) ||
page.offset !== offset ||
page.limit !== PAGE_LIMIT ||
page.total !== expectedCount ||
page.items.length !== Math.min(PAGE_LIMIT, expectedCount - offset)
)
throw new Error("Hopper returned an inconsistent fixture search page");
const expectedNext =
offset + page.items.length < expectedCount
? offset + page.items.length
: null;
if (
page.next_offset !== expectedNext ||
page.has_more !== (expectedNext !== null)
)
throw new Error("Hopper returned inconsistent pagination metadata");
}

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