Compare commits

...
Author SHA1 Message Date
Kaoru0822andClaude Opus 5.5 0c69564297 fix(electron): leave the preload unknown after a later dynamic override
A BrowserWindow webPreferences literal kept an earlier literal preload
after a later preload with a non-literal value, a spread, a computed key
or an accessor could replace it, so the graph associated the window with
the overwritten script. Derive the effective preload from the last
definition of the property, as Electron receives it.

Closes #1237

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:55:33 +00:00
YsopwnableandGitHub 0c6852b392 docs(ghidra): explain supported heap and CPU controls (#1278)
Explain consumed heap settings, owned JVM environment overrides, CPU affinity and memory measurements in the Ghidra setup guide.

Closes #1255
2026-10-09 21:20:33 +08:00
YsopwnableandGitHub f085589957 fix(ci): pass selected environment to Windows Ghidra fixture (#1281) 2026-10-09 13:05:54 +00:00
morluto d6d1cd2485 test: remove duplicate workflow examples and self assertions 2026-10-09 21:01:53 +08:00
morluto af7a439633 fix(windows): retain runtime authority after cleanup failure 2026-10-09 21:01:53 +08:00
morluto 81b70bc358 fix(managed): preserve literal library names during native matching 2026-10-09 21:01:52 +08:00
morluto 5eb0049a9f fix(process): retain known events from incomplete captures 2026-10-09 21:01:52 +08:00
morluto b7b4333703 fix(reference)!: represent unreadable symlink targets explicitly
BREAKING CHANGE: Historical source symlinks with unreadable or unknown target_state require target: null. Placeholder target strings are rejected.
2026-10-09 21:01:52 +08:00
morluto 0696bde266 fix(providers): preserve observations and primary cleanup failures 2026-10-09 21:01:52 +08:00
morluto 88640f60fe fix(browser): retain scenario resources until cleanup succeeds 2026-10-09 21:01:52 +08:00
YsopwnableandGitHub 636b245f1b fix(android): support owned JADX stdio on Windows (#1276)
Support owned JADX stdio on Windows x64, preserve protocol input errors through cancellation, and verify native stdin and Android CLI/MCP lifecycle on the real Windows host.

Closes #1262
2026-10-09 20:34:42 +08:00
morluto b71a591a60 test: consolidate redundant assertions into boundary workflows
Remove getter, enum, cloned-projection, cache-identity, and obsolete profile assertions along with the shadow JSON Schema engine and dead test-only APIs. Retain producer, filesystem, SDK, lifecycle, integrity, and malformed-format coverage.

Document the priority of real end-to-end workflows, boundary integration, and producer goldens.
2026-10-09 20:26:46 +08:00
morluto 2f02eff988 fix(managed): separate metadata and signature completeness 2026-10-09 20:18:52 +08:00
morluto 5510b27d7f refactor(domain)!: remove obsolete comparison representations
Compare complete function dossiers directly, remove unreachable truncation and collection wrappers, and preserve deterministic Unicode ordering. Require real external reference targets instead of display placeholders.

BREAKING CHANGE: function comparison results no longer advertise the unreachable truncated state or counter.
2026-10-09 20:18:52 +08:00
morluto 475a0f84da fix(javascript)!: preserve property paths and uncertain slot presence
Use one explicit object/array slot model and JSON-pointer property identity across mutation analysis, return projection, and semantic graphs. Require observed presence for discriminants and resolve writes only through retained object or array receivers.

BREAKING CHANGE: projected return fields require explicit presence and semantic object operations carry full property paths.
2026-10-09 20:18:52 +08:00
morluto 63bf91405c refactor!: enforce canonical evidence and provider boundaries
Deliver complete Evidence directly through MCP with required nullable analysis profiles and immutable per-server delivery settings. Bind selected environments at construction and require typed cleanup outcomes with retained retry ownership.

Remove test-only routing, duplicate session and snapshot metadata, producer-field repairs, and ambiguous provider arguments. Separate artifact byte identity from occurrence classification and retain typed source-map and partial native facts across CLI, MCP, and verifier consumers.

BREAKING CHANGE: obsolete Evidence envelopes, session aliases, incomplete snapshots, and missing normalized producer facts are no longer accepted.
2026-10-09 20:18:52 +08:00
morluto 2a7746998d fix(process)!: require current captures and preserve cleanup failures
Remove v3 capture conversion and require explicit executable identity and event journals. Preserve cancellation, execution diagnostics, snapshots, and cleanup outcomes from preparation through CLI delivery.
2026-10-09 20:12:07 +08:00
morluto abc4f47683 build: separate runtime artifacts and supervise command ownership 2026-10-09 20:11:17 +08:00
YsopwnableandGitHub c8544b1574 fix(process): preserve unknown filesystem absence in partial captures
Separate exhausted path enumeration from hash coverage and preserve known
effects outside incomplete roots or unfollowed symlink subtrees. Return
explicit unknown effects through capture Evidence and comparison workflows.

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-09 12:09:24 +00:00
YsopwnableandGitHub 4578647c59 Merge pull request #1272 from morluto/docs/website-calculator-continuation
docs(website): continue the Calculator example with reconstruction
2026-10-09 19:57:38 +08:00
morluto cc39fc53b8 feat(website): add Discord link beside GitHub badge 2026-10-09 19:54:34 +08:00
YsopwnableandGitHub 13c3b68730 Merge pull request #1270 from morluto/fix/batch-procedure-identity
fix(native): distinguish batch selectors from resolved entries
2026-10-09 19:39:48 +08:00
N0zoM1z0 18a1e8cd0e docs(website): continue the Calculator example with reconstruction 2026-10-09 19:38:17 +08:00
N0zoM1z0 933fcb33c1 test(native): assert batch identity in production registration 2026-10-09 19:27:27 +08:00
YsopwnableandGitHub f6fe575094 fix(process): await cleanup on CLI and MCP cancellation
Propagate CLI interrupts to capture and interrupt post-exit settlement
without cancelling owned-resource cleanup or discarding prior observations.

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-09 11:26:41 +00:00
N0zoM1z0 8342e3a78f chore: merge current main into batch identity fix 2026-10-09 19:13:13 +08:00
N0zoM1z0 bfaa5a6c2c fix(native): distinguish batch selectors from resolved entries 2026-10-09 19:09:30 +08:00
YsopwnableandGitHub b189c3ceb1 Merge pull request #1268 from morluto/fix/ghidra-return-flow
fix(ghidra): preserve decoded return flow evidence
2026-10-09 19:04:47 +08:00
N0zoM1z0 bcc8a773e2 fix(ghidra): preserve decoded return flow evidence 2026-10-09 18:53:28 +08:00
YsopwnableandGitHub 6cc817e8bd Merge pull request #1266 from morluto/feat/website-apk-showcase
feat(website): add an Android showcase and clearer project prompts
2026-10-09 18:46:52 +08:00
keemsisiandGitHub 9426c3f1bb perf(hopper): resolve string search records once per query (#1229)
* perf(hopper): resolve string search records once per query

search_strings called _string_inventory(document) once per match, and each
call sorts every string address and rebuilds the record dict, so a query cost
O(matches x strings log strings). Resolve the inventory once per search.
Warm search, 50% matches, fake document, best of 7: 1000 strings 186 -> 0.5 ms,
4000 strings 3161 -> 2.1 ms, 8000 strings 13121 -> 4.9 ms; output byte-identical.

* perf(hopper): skip record resolution for empty string searches

Resolving the full string inventory for a query with no matches added a sort
of every string address that the previous code never performed. Return early
when nothing matches. Zero-match search on a warm document, best of 7:
64000 strings 35.9 ms before this change, 4.0 ms after (main: 3.9 ms).
2026-10-09 18:39:19 +08:00
N0zoM1z0 5691606aa9 fix(website): clarify Android host and JDK requirements 2026-10-09 18:31:22 +08:00
45e3edc17f feat(javascript): report observed export property presence (#1160)
* feat(javascript): report observed export property presence

Extend compare_javascript_export_shapes so each retained return variant
lists observed property names and each change reports presence independently
of value resolution. Complete parent coverage classifies one-sided names as
added or removed even when static values stay unknown.

Fixes #725

* test(javascript): match unknown export-shape values by availability

Nested availability objects include a reason when values stay unresolved.
Assert availability through objectContaining so presence-only added and
removed rows still verify unknown values.

* fix(javascript): preserve uncertain export property presence and locations

* fix(javascript): normalize presence and preserve local property coverage

* fix(javascript): require present discriminants for return pairing

* fix(javascript): normalize retained shapes before comparison

* refactor(javascript): share return projection with executable examples

* fix(javascript): localize uncertainty for missing mutated slots

* fix(javascript): bind advertised export examples to exact artifacts

* fix(javascript): keep uncertain property flow as candidates

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-09 18:30:31 +08:00
YsopwnableandGitHub 04aef6df33 Merge pull request #1265 from morluto/fix/snapshot-receipt-counts
fix(snapshot): report query workflow and Evidence counts separately
2026-10-09 18:29:02 +08:00
YsopwnableandGitHub 552634d634 Merge pull request #1264 from morluto/perf/snapshot-materialization
perf(snapshot): avoid redundant result copies and serialization
2026-10-09 18:19:59 +08:00
N0zoM1z0 0e779533a9 fix(snapshot): report query workflow and Evidence counts separately 2026-10-09 18:13:50 +08:00
YsopwnableandGitHub c5c75c38c3 Merge pull request #1263 from morluto/fix/process-normalization-fidelity
fix(process): preserve numeric output and original PTY evidence
2026-10-09 18:07:22 +08:00
N0zoM1z0 569d2bc6b9 perf(snapshot): avoid redundant result copies and serialization 2026-10-09 18:07:21 +08:00
889fb374df fix(javascript): match source-map originals that name parent directories (#1215)
* fix(javascript): match source-map originals that name parent directories

* fix(javascript): resolve parent segments against the source map's directory

* fix(javascript): resolve source originals against their map location

* fix(javascript): preserve URI and empty source map path semantics

* fix(javascript): separate source URL paths from opaque URI payloads

* fix(javascript): preserve filesystem names during URI classification

* style(javascript): format source URI presence guard

* fix(javascript): resolve source map references with URL semantics

* fix(javascript): preserve literal UNC source map paths

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-09 18:04:25 +08:00
YsopwnableandGitHub 24a3aa08d1 fix(session): retain failed provider cleanup for lifecycle retries (#1260)
Retire inactive clients until cleanup succeeds and block replacement startup
while owned resources remain. Preserve startup/cancellation failures alongside
cleanup diagnostics and retain explicit document choices across retries.

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-09 17:56:17 +08:00
N0zoM1z0 00d8e44cbf fix(process): preserve numeric output and original PTY evidence 2026-10-09 17:55:28 +08:00
N0zoM1z0 3fb9ffaa4d docs(website): invite deeper projects and reader feedback 2026-10-09 17:48:59 +08:00
Kaoru0822andGitHub db0afce5f3 Merge pull request #1259 from morluto/fix/cli-json-input-read-cause
fix(cli): list the read failure cause for an unreadable JSON input file
2026-10-09 02:47:42 -07:00
Kaoru0822andGitHub 5612496f6d Merge pull request #1258 from morluto/fix/inspect-plist-default-remediation
fix(native): name the plist selection that works when Info.plist is missing
2026-10-09 02:47:30 -07:00
Kaoru0822andGitHub fe5291e916 Merge pull request #1239 from morluto/fix/javascript-input-selection-errors
fix(javascript): classify application input selection failures by cause
2026-10-09 02:47:09 -07:00
Kaoru0822andGitHub d4a598e936 Merge pull request #1256 from morluto/fix/evidence-file-selection-code
fix(errors): report caller-selected evidence path failures as invalid input
2026-10-09 02:46:04 -07:00
Kaoru0822andGitHub ddaa18301a Merge pull request #1240 from morluto/fix/web-script-export-capture-selection
fix(browser): classify export-web-scripts capture selection failures by cause
2026-10-09 02:45:47 -07:00
morluto d6832c3632 docs: require focused brittleness cleanup and executable examples 2026-10-09 17:41:14 +08:00
Kaoru0822andClaude Opus 5.5 9b6950373c fix(native): name the CLI flag that selects another plist
rea inspect-plist takes the target as its positional path and selects a
plist with --relative-path, so "Pass path" pointed CLI callers at the
wrong argument. Name the MCP path input and the CLI flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:32:24 +00:00
Kaoru0822andClaude Opus 5.5 6020df53d9 fix(cli): list the read failure cause for an unreadable JSON input file
A CLI workflow given a JSON input file that cannot be read (missing, a
directory, or permission denied) returned invalid_request with
input_reason "read-failed" but no details. The remediation "Correct the
listed arguments and retry." therefore listed nothing, and the system
error that separates a typo from a permission denial was dropped.

Add an invalid_value issue naming the system error code and the path.
The envelope, code and input_reason are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:30:15 +00:00
Kaoru0822andClaude Opus 5.5 a6fdf426bd fix(native): name the plist selection that works when Info.plist is missing
inspect_plist on a target without a readable default Contents/Info.plist
(for example a bare Mach-O) reported the generic "This analysis is
unavailable for the current target. Choose another analysis or target."
as message and remediation. The specific reason, including "Pass path to
inspect another plist", appeared only in details.reason, although
selecting another plist in the same target is the available workflow.

Pass that reason as the refusal's user message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:26:18 +00:00
Kaoru0822andClaude Opus 5.5 db177c91b8 fix(errors): report caller-selected evidence path failures as invalid input
An EvidenceFileError for a missing evidence file, a selected path that is
not a regular file, or an output that already exists projected code and
category execution_failure. The message already told the caller to
correct the path, but agents branching on code or category saw a tool
failure.

Project those three reasons as invalid_request with category
invalid_input. An unreadable file (io) and invalid JSON keep their
codes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:23:00 +00:00
Kaoru0822andClaude Opus 5.5 d186ca7e41 test(javascript): expect the invalid input_path code in the streamed CLI failure test
The acceptance test uses a missing input path only to produce a streamed
JSON failure with a nonzero exit. The missing path is now an invalid
input_path rather than an artifact io failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:20:16 +00:00
YsopwnableandGitHub aa5750c263 fix(mcp): emit portable Unicode NUL patterns (#1230)
Preserve NUL rejection while avoiding the short escape rejected by strict
schema consumers. Guard the entire advertised input/output catalog and all
three JavaScript regexp modes.

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-09 17:18:17 +08:00
Kaoru0822andClaude Opus 5.5 8c5e34cd37 fix(browser): classify export-web-scripts capture selection failures by cause
export_web_scripts reported a missing capture, a directory selected as
the capture, and an unreadable capture as an artifact io failure ("Script
export failed for <capture> -> <output>: ENOENT ..."), unlike its sibling
inspect_web_network_capture, which already reports these as invalid
input or access_denied.

Read the selected capture in its own step: ENOENT, ENOTDIR and EISDIR
become an invalid capture_path issue, and EACCES or EPERM becomes
access_denied. Failures while writing the export keep their existing
classification.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:16:59 +00:00
N0zoM1z0 0270dd5739 feat(website): add an Aegis APK code-generation showcase 2026-10-09 17:15:07 +08:00
Kaoru0822andClaude Opus 5.5 0a14bbbf05 fix(javascript): classify application input selection failures by cause
analyze_javascript_application reported every input-selection problem as
an artifact fault:

- a missing input path: "Artifact could not be read or written", with no
  path or reason;
- an unreadable selected directory: the same generic io failure;
- a regular file that is neither a directory nor an .asar, and a
  --artifact-format that contradicts the selection: "Artifact is invalid
  or has changed. Get a fresh copy and try again."

Report a missing path as an invalid input_path, an unreadable selection
as access_denied, a non-directory non-.asar file as unsupported_target
with the accepted inputs as remediation, and a contradicting format as an
invalid format with the observed kind as expected value. Failures inside
a selected artifact keep their existing classification.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:13:17 +00:00
N0zoM1z0 f6f5c16097 docs(website): introduce the worked homepage examples 2026-10-09 16:52:45 +08:00
Kaoru0822andGitHub f4c99ab8a5 Merge pull request #1228 from morluto/fix/keyed-archive-host-remediation
fix(artifacts): name only host-available workflows for non-archive plists
2026-10-09 01:48:51 -07:00
954badbaaa refactor(schemas): share portable identifier and base64 patterns (#1227)
* refactor(schemas): share identifier and canonical base64 patterns

* test: hold the offset datetime schema to the validator it replaces

The module documents that its accepted set matches z.string().datetime({ offset: true }), and nothing in the suite checked that claim. Adds table-driven accept and reject cases (leap years including the 1900/2000 century pair, month, day, hour and offset ranges, fractional and omitted seconds, missing offset, basic format) plus a differential assertion that classifies every case exactly as the replaced validator does, and asserts the emitted pattern compiles under u and v while the replaced pattern does not compile under v.

---------

Co-authored-by: felipeatp <felipe.troncosopt@gmail.com>
2026-10-09 16:47:09 +08:00
Kaoru0822andClaude Opus 5.5 22958558ea fix(artifacts): name only host-available workflows for non-archive plists
inspect_keyed_archive runs on every host, but its remediation for an
ordinary property list pointed to inspect_plist, which only macOS
supports. On Linux that sent callers to a workflow the capability layer
rejects.

The domain error now names what the file is. The artifact reader picks
the remediation from the provider's host: inspect_plist on macOS, and
elsewhere the selection of an NSKeyedArchiver archive, with a note that
inspect_plist requires macOS. decode_interface_builder, which is
portable, stays the NIBArchive remediation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 08:40:45 +00:00
Kaoru0822andGitHub fef38e4d22 Merge pull request #1226 from morluto/fix/keyed-archive-kind-refusal
fix(artifacts): report non-archive keyed-archive selections as unsupported targets
2026-10-09 01:37:29 -07:00
Kaoru0822andGitHub 136d4aab18 Merge pull request #1225 from morluto/fix/exec-stop-timeout-validation
fix(process): refuse malformed timeouts for stoppable commands
2026-10-09 01:33:25 -07:00
Kaoru0822andGitHub 68cb02fe8b Merge pull request #1224 from morluto/fix/adapter-host-refusal-messages
fix: name host and tool requirements in optional adapter refusals
2026-10-09 01:33:15 -07:00
Kaoru0822andClaude Opus 5.5 8c5088c8cd fix(artifacts): report non-archive keyed-archive selections as unsupported targets
inspect_keyed_archive reported a selected ordinary property list or a
compiled NIBArchive as a damaged artifact: "Artifact is invalid or has
changed. Get a fresh copy and try again." The file is intact; it is
another kind of file, and the workflow that reads it (inspect_plist or
decode_interface_builder) appeared only in details.detail.

Report a NIBArchive, or a plist whose $archiver is not NSKeyedArchiver,
as unsupported_target with that workflow as the remediation. An
NSKeyedArchiver archive that fails to decode stays a format failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 08:28:21 +00:00
Kaoru0822andClaude Opus 5.5 3d328193d6 fix(process): apply one timeout rule to both execFileOutput modes
Node's execFile timeout contract differs by supported runtime: Node 22
and 24 accept any non-negative integer (their timers clamp anything
above 2^31-1 to 1 ms), while Node 26 caps it at Number.MAX_SAFE_INTEGER.
Mirroring either left stoppable and plain commands disagreeing on one
runtime. Validate a safe non-negative integer before spawning in both
modes, so every supported runtime gives the same answer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 08:22:09 +00:00
felipeatpandGitHub f0da05f67f fix: emit v-mode-compatible patterns for tool schemas (#1220)
* fix: emit v-mode-compatible patterns for tool schemas

Character classes in emitted patterns carried unescaped '-', '/' and '['. Annex-B and u-mode accept them, so REA's own Zod validation (u) and every u-based validator never saw a defect, but v-mode rejects ClassSetSyntaxCharacters and Ruby rejects [^s[]]. With 6.1.0 tools/list, 117 pattern fields across 11 tools were rejected under v.

The characters are escaped as \x2d, \x2f, \x5b and \x5d rather than \-/\//\[, because oxlint --fix removes an escaped '-' inside a character class as a useless escape, so an escape-only change does not survive the pre-commit hook. \x2d-style escapes are valid under Annex B, u and v, are matched identically, and are not useless-escape candidates.

22 pattern literals in 18 files are touched; no other token changes.

* fix: replace the Zod offset pattern and guard the advertised catalog

The offset datetime site projected Zod's offset class as [ +- ], which no escape in this repository's sources can reach, so evaluate_reconstruction_coverage stayed unreachable for exactly the clients this change targets. It now uses offsetDateTimeSchema in src/domain/offsetDateTime.ts: the same accepted set with the offset class escaped as [ +\x2d ].

Adds tests/boundary/mcp/advertisedPatternsVMode.test.ts, which performs the real tools/list projection, traverses every advertised input and output schema, compiles each pattern under v, and asserts the inventory it walked so a silent traversal collapse fails instead of passing vacuously.
2026-10-09 16:20:57 +08:00
Jay HemnaniandGitHub 01174c4a81 fix(javascript): report located sources with differing digests as modified (#1216) 2026-10-09 16:20:42 +08:00
Kaoru0822andClaude Opus 5.5 6489d58851 fix(javascript): keep Wakaru version stderr out of the refusal message
The version-mismatch reason ends with the tool's complete stderr, which
is too long for a caller-facing message. The message now names the
expected and reported version and exit code; details.reason keeps the
stderr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 08:16:45 +00:00
Kaoru0822andClaude Opus 5.5 59c91c88fe fix(process): refuse malformed timeouts for stoppable commands
Since a stoppable command keeps its own deadline, execFile no longer
validated its timeout. A negative, NaN, fractional or infinite value
then started the command with no deadline (or one setTimeout coerced)
instead of failing with ERR_OUT_OF_RANGE as execFile does.

Apply execFile's unsigned-integer contract before spawning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 08:15:03 +00:00
c89b3a00dc test(artifacts): read /bin/ls slice layout from its FAT table (#1223)
The real-lipo slice test pinned the byte offsets and sizes of one macOS
release's /bin/ls, so it failed on any host whose /bin/ls differs
(macOS 26.7.1: x86_64 48128 bytes, arm64e 88672, versus the pinned 48112
and 89088). Read the expected layout from the binary's FAT header
instead.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:12:27 +08:00
Kaoru0822andClaude Opus 5.5 170d0bf9e2 fix: name host and tool requirements in optional adapter refusals
Host and tool-configuration refusals from the pwntools, EVMole, firmware,
mitmproxy, Wakaru, LLDB and xcrun adapters carried no userMessage, so CLI
and MCP callers saw the generic "This analysis is unavailable for the
current target. Choose another analysis or target." as both message and
remediation. Choosing another target cannot satisfy a Linux-only profile
or an unset REA_*_COMMAND. The actual requirement appeared only in
details.reason.

Pass each refusal's reason as its userMessage, as the adapters' other
configuration refusals (EVMole limits, JADX, pwndbg) already do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 08:12:25 +00:00
6935dc7fe1 fix(ghidra): classify modern Swift manglings in swift type analysis (#1208)
* fix(ghidra): classify modern Swift manglings in swift type analysis

Categorize modern Swift 4.2+ manglings ($s, _$s) instead of dropping them
in analyze_swift_types. The classifier only accepted the legacy _Tt marker,
so procedure names reported by current providers produced an empty inventory.
Legacy _Tt families and behavior are unchanged. Fixes #1187.

* fix(swift): retain provider aliases and category unknowns

---------

Co-authored-by: Saim Shafique <sx4imm@gmail.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-09 16:10:55 +08:00
Kaoru0822andGitHub 04a516b1e3 Merge pull request #1222 from morluto/fix/ghidra-relative-java-probe
fix(ghidra): do not run Java from a relative JAVA_HOME in doctor and setup
2026-10-09 01:04:53 -07:00
Kaoru0822andGitHub 4b38d18995 Merge pull request #1221 from morluto/fix/exec-stop-signal-timeout
fix(process): stop timed-out commands with the caller's stop signal
2026-10-09 01:04:21 -07:00
Kaoru0822andClaude Opus 5.5 c3f169823f fix(ghidra): do not run Java from a relative JAVA_HOME in doctor and setup
The installation inspection rejected a relative JAVA_HOME, but only after
it had already probed `<JAVA_HOME>/bin/java` with spawnSync. `rea doctor`
and `rea setup` therefore ran whatever `jdk/bin/java` (or `bin/java` for
an empty JAVA_HOME) the current directory held, while reporting the
setting as rejected.

Skip the probe when JAVA_HOME is not absolute on the inspected platform.
The configuration check still reports the rejection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 07:59:38 +00:00
Kaoru0822andClaude Opus 5.5 063de5014f fix(process): stop timed-out commands with the caller's stop signal
execFileOutput forwarded stopSignal only together with an AbortSignal, so
a caller with just a timeout (the native UI helper's swiftc build) still
got Node's SIGTERM. When the timeout did send the stop signal, nothing
killed a process that ignored it, and a process that exited cleanly
after the signal resolved as a success.

A stoppable command now owns its timeout: it sends the stop signal,
kills the process after the grace period, and rejects with ETIMEDOUT
and killed: true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 07:57:14 +00:00
morluto e6905c18e0 fix(website): identify publication verifier requests 2026-10-09 15:46:20 +08:00
YsopwnableandGitHub 9405bd660d fix(javascript): allow cancellation during semantic graph commitment (#1218)
Move whole-graph commitment and batched integrity checks into owned domain steps while retaining eager input capture, canonical IDs, full validation and completion-only immutable proofs.

Verified complete real Notion identity parity and actual stdio MCP cancellation, Evidence retention and connection reuse.

Refs #1065.
2026-10-09 15:15:17 +08:00
27a9ceade7 fix!: restore caching, preserve failures, and verify fixture claims (#1211)
Restore profile-bound Ghidra snapshot reuse, preserve direct-analysis results through cleanup failures, and grade configured agent fixture claims against returned evidence.

Register the compiled agent-evaluation fixture entry for static analysis.

Co-authored-by: YouWish <29498139+Y0UW1SH@users.noreply.github.com>
Co-authored-by: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com>
2026-10-09 14:49:09 +08:00
YsopwnableandGitHub c399670653 docs: keep the 30k milestone in Star History (#1217) 2026-10-09 14:45:59 +08:00
YsopwnableandGitHub 863674b2e9 feat(website): add homepage navigation and faster reading paths (#1213)
feat(website): add homepage navigation and faster reading paths
2026-10-09 14:33:47 +08:00
YsopwnableandGitHub c2d8146f94 docs: clarify REA has no affiliated cryptocurrency or token (#1214) 2026-10-09 14:25:32 +08:00
N0zoM1z0 c94f06d98f feat(website): add homepage navigation and faster reading paths 2026-10-09 14:03:00 +08:00
Kaoru0822andGitHub 5b769d3a78 Merge pull request #1201 from morluto/fix/ghidra-runtime-cleanup-retry
fix(ghidra): keep and name a runtime directory that close could not remove
2026-10-08 22:49:12 -07:00
Kaoru0822andGitHub 6987cb066a Merge pull request #1207 from morluto/docs/celebrate-30000-stars
docs: celebrate 30,000 GitHub stars 🎉
2026-10-08 22:48:54 -07:00
YsopwnableandGitHub 7e9c91199e fix(cli): resolve xrefs symbol selectors after address validation (#1209)
* fix(cli): resolve xrefs symbol selectors after address validation

* style(cli): format xrefs fallback call
2026-10-09 13:28:48 +08:00
N0zoM1z0 9db1fd0bd7 docs: thank feature request contributors 2026-10-09 13:02:26 +08:00
YsopwnableandGitHub 796b65e1cd ci(website): allow explicitly requested Pages-only publication (#1205)
ci(website): allow explicitly requested Pages-only publication
2026-10-09 13:00:26 +08:00
N0zoM1z0 beca9977eb docs: celebrate 30,000 GitHub stars 🎉 2026-10-09 12:58:16 +08:00
YsopwnableandGitHub 6051cb2c0c fix(setup): preserve initial backups and Codex comments (#1204) 2026-10-09 12:57:00 +08:00
N0zoM1z0 864160d885 ci(website): allow explicitly requested Pages-only publication 2026-10-09 12:46:17 +08:00
db675f1d52 feat(mcp): select views of retained analysis results (#1180)
Add explicit summary, facet, item and paged views of retained analysis Evidence without repeating analysis. Preserve captured observations, graph validation, artifact identity, coverage and complete default results.

Fixes #1050

Verified with focused source, CLI, pinned-SDK schema and MCP tests, serial static checks, and one complete native Windows Notion ASAR workflow. Real Hopper/Ghidra verification was not performed for this provider-independent projection change.

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-10-09 12:45:48 +08:00
911152a04d fix(cli): complete cleanup after relayed interrupts (#1202)
* fix: Handle relayed SIGINT gracefully in withProcessCancellation

Changes `process.once` to `process.on` for SIGINT in `DirectAnalysis.ts`.
When REA is launched via npx, a single process-group SIGINT triggers two
SIGINT events (one direct, one relayed). Previously, the first event would
consume the handler and the second would abruptly terminate the Node process,
abandoning Ghidra resource cleanup.

This change ensures subsequent SIGINTs idempotently request cancellation
without crashing the process, matching the behaviour of `withCommandCancellation`.

* fix(cli): keep termination guards through provider cleanup

* test(cli): exercise repeated analysis signals on macOS

---------

Co-authored-by: N0zoM1z0 <1705004398@qq.com>
2026-10-09 12:45:23 +08:00
YsopwnableandGitHub 0fec65d06b feat(website): add beginner examples and synchronized publishing (#1203)
feat(website): add beginner examples and synchronized publishing
2026-10-09 12:41:51 +08:00
Kaoru0822andGitHub cf415619ca Merge pull request #1194 from morluto/fix/darwin-helper-compile-tmpdir
fix(process): stop cancelled Swift helper builds before removing their files
2026-10-08 21:21:32 -07:00
Kaoru0822andGitHub cfb49eb8a8 Merge pull request #1198 from morluto/fix/ghidra-extension-unsupported-host
fix(ghidra): report NativeAOT host and target refusals as unsupported, not failures
2026-10-08 21:21:18 -07:00
Kaoru0822andGitHub 8998438c23 Merge pull request #1200 from morluto/fix/doctor-setup-relative-provider-paths
fix(ghidra): reject relative installation and Java paths in doctor and setup
2026-10-08 21:21:04 -07:00
Kaoru0822andGitHub c3acc34475 Merge pull request #1199 from morluto/fix/config-error-names-setting
fix(config): name the rejected setting and its constraint in configuration errors
2026-10-08 21:20:48 -07:00
N0zoM1z0 761881cec3 ci(website): publish Cloudflare and Pages from one artifact 2026-10-09 12:19:38 +08:00
Kaoru0822andClaude Opus 5.5 b79d379c25 fix(ghidra): keep and name a runtime directory that close could not remove
When removing Ghidra's private runtime directory failed, close_binary
reported only a generic `provider-client` resource, and a later close,
after the permission was restored, returned success while the directory
and its contents remained. The client dropped its reference to the root
before removing it, and PrivateRuntimeRoot cached the rejected removal.

Keep each runtime or socket root until its removal succeeds, report a
failure as a ProviderCleanupError naming the leftover paths, let
PrivateRuntimeRoot retry a failed removal, and pass such typed cleanup
failures through closeAnalysisClient instead of replacing them.

Fixes #1190

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 04:19:10 +00:00
YsopwnableandGitHub 6fbdbd6820 fix(ghidra): resolve secondary symbols at function entries (#1195)
* fix(ghidra): resolve secondary symbols at function entries

* test(ghidra): derive interior bytes from decoded instructions
2026-10-09 12:19:07 +08:00
Kaoru0822andClaude Opus 5.5 53568568bf fix(ghidra): reject relative installation and Java paths in doctor and setup
A relative GHIDRA_INSTALL_DIR (or JAVA_HOME) that resolved from the current
directory passed doctor's Ghidra inspection, so doctor reported a healthy
provider and setup wrote the relative path into the MCP registration, but
REA's configuration parser rejects relative paths, so `providers`,
`capabilities` and the registered MCP server all failed.

The installation inspection's configuration check now applies the same
absolute-path rule, judged by the inspected platform's path syntax, and
names the relative setting. Doctor reports the provider unavailable with
that reason, and setup, which registers only an available inspection's
environment, no longer writes the path.

Fixes #1183

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 04:13:40 +00:00
Kaoru0822andGitHub f36c10f9e8 Merge pull request #1197 from morluto/fix/target-admission-access-denied
fix(domain): report target read-permission denials as access_denied
2026-10-08 21:13:32 -07:00
Kaoru0822andGitHub d39a450bdf Merge pull request #1196 from morluto/fix/target-truncated-headers
fix(native): refuse truncated executable headers at target admission
2026-10-08 21:13:15 -07:00
Kaoru0822andGitHub 98b07d07dd Merge pull request #1182 from morluto/fix/doctor-hopper-discovery-readonly
fix(doctor): find the Homebrew Hopper cask without writing Homebrew caches
2026-10-08 21:13:00 -07:00
Kaoru0822andClaude Opus 5.5 f198993f41 fix(ghidra): keep the NativeAOT target refusal's own recovery advice
An unsupported-target error always projected the generic "select a target"
remediation, which hid the adapter's available workflows (omit
REA_GHIDRA_NATIVEAOT_JAR for ordinary analysis, or use
inspect_managed_artifact). AnalysisUnsupportedTargetError accepts a
refusal-specific remediation, as AnalysisResourceConstraintError does, and
the NativeAOT refusal supplies its reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 04:09:50 +00:00
N0zoM1z0 54c0ed42aa chore: merge main before website publication 2026-10-09 12:09:29 +08:00
Kaoru0822andClaude Opus 5.5 0a0244db42 fix(config): name the rejected setting and its constraint in configuration errors
Every configuration failure projected the same message, "REA configuration
is invalid. Run `rea doctor` and fix the reported setting.", with no
details. The environment parser discarded the issue that names the variable
(for example `REA_GHIDRA_NATIVEAOT_JAR must be absolute`), and doctor can
report healthy for that environment, so neither the CLI, the MCP startup
message, nor doctor told callers which setting to correct.

ConfigurationError now carries the rejected settings and their constraints
(never their values). The projection names them in its message, lists them
in `details.settings`, and asks the caller to correct those settings where
REA is started. Errors without a named setting keep the previous message.

Fixes #1184

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 04:08:43 +00:00
Kaoru0822andClaude Opus 5.5 9f5dc52be8 fix(hopper): keep the OS error when the FAT64 source cannot be reopened
resolveHopperMachOImage built its BinaryTargetError without the open
failure as its cause, so a target that became unreadable after admission
still projected as `target_unavailable` instead of `access_denied`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 04:04:42 +00:00
Kaoru0822andClaude Opus 5.5 42c276af1e fix(ghidra): report NativeAOT host and target refusals as unsupported, not failures
With REA_GHIDRA_NATIVEAOT_JAR set on a host other than Linux, Ghidra
analysis was refused before Ghidra started, as intended, but the refusal
was a ProviderAdapterError: `execution_failure` with advice to retry and
run doctor, neither of which can change the host.

Adapters now say whether a refusal concerns the host or the target. A host
refusal is `capability_unavailable` whose message and remediation are the
adapter's reason (omit the JAR for ordinary analysis); a target refusal is
`unsupported_target` with the target path. Missing or malformed JARs on a
supported host remain provider failures.

Fixes #1185

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 04:03:10 +00:00
N0zoM1z0 291bc6fb70 feat(website): show manual analysis beside a one-prompt workflow 2026-10-09 12:02:58 +08:00
Kaoru0822andClaude Opus 5.5 facaae865a test(native): build the .app fixture executable with a complete Mach-O header
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 03:59:19 +00:00
Kaoru0822andClaude Opus 5.5 d0ff57a45c fix(domain): report target read-permission denials as access_denied
Opening a target the current user cannot read returned
`target_unavailable`, the same code as a missing target, although the
reason named EACCES. Clients that branch on the error code could not choose
read-access recovery, contrary to the boundary contract that separates host
permission denial from unavailable targets.

A BinaryTargetError whose cause is an OS EACCES or EPERM now records that
system code and projects as `access_denied` with `system_code`, the
filesystem-read boundary, and read-access remediation, while keeping its
specific reason and path. This covers target files, app bundle paths and
Info.plist reads, and selected plist reads, since every site passes the OS
error as the cause.

Fixes #1188

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 03:56:38 +00:00
Kaoru0822andClaude Opus 5.5 0b04d384cc fix(native): refuse truncated executable headers at target admission
Target admission accepted a thin Mach-O after reading only its magic and
CPU type, so a 12-byte prefix opened as a valid arm64 executable and the
first provider query failed as a generic execution failure. ELF headers
were likewise accepted after 20 bytes, and a FAT file whose selected slice
lies beyond its end was admitted.

Require the complete fixed header (Mach-O 28 or 32 bytes, ELF 52 or 64),
Mach-O load commands that fit in the file, and a selected FAT slice within
the file, checking commitments against the file size rather than the
probed prefix. Each refusal names the failed constraint.

Fixes #1189

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 03:51:37 +00:00
Kaoru0822andGitHub e6abaefbbc Merge pull request #1179 from morluto/fix/session-client-metadata-2025
fix(server): report 2025-era client identity and features in binary_session
2026-10-08 20:51:34 -07:00
Kaoru0822andGitHub f5539cfc2f Merge pull request #1178 from morluto/fix/codex-toml-preserve-comments
fix(setup): keep Codex TOML comments when adding or removing REA
2026-10-08 20:51:15 -07:00
2a620c06cd fix(javascript): resolve package exports targets only to the exact file (#1181)
A selected package exports target names one file, and Node tries no
extension or directory index for it. REA still resolved it like a relative
specifier, so `exports: "./file"` with only `file.js` present, or
`"./folder"` with only `folder/index.js`, reported a resolved import that
Node rejects under both import and require.

Resolve an exports target only to its exact file and otherwise report
not-found with a limitation naming the target. Legacy main entries keep
their extension and index fallbacks.

Fixes #1174

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:49:32 +08:00
N0zoM1z0 e750c262fb feat(website): clarify reverse engineering and showcase goals 2026-10-09 11:46:06 +08:00
Kaoru0822andClaude Opus 5.5 c717204795 fix(process): stop cancelled Swift helper builds before removing their files
Cancelling the macOS process-ownership helper build left swiftc object files
in TMPDIR. Node's execFile sends SIGTERM on abort and rejects before the
child exits, and swift-driver runs each frontend job in its own process
group: on SIGTERM the driver exits while a job keeps writing, so REA removed
its root while objects were still being written outside and inside it.

Add an opt-in `stopSignal` to execFileOutput that stops the process with the
caller's signal on abort or timeout and settles only after it exits. Swift
helper builds stop swiftc with SIGINT, on which swift-driver stops its jobs
and deletes their objects, and point TMPDIR inside the root cleanup removes.

Fixes #1173

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 03:43:43 +00:00
YsopwnableandGitHub e1afbc1914 perf(mcp): reuse input schemas without losing caller guidance (#1162) 2026-10-09 11:38:07 +08:00
Kaoru0822andClaude Opus 5.5 a503376b2f fix(doctor): find the Homebrew Hopper cask without writing Homebrew caches
`rea doctor`, documented as strictly read-only, ran
`brew --prefix --cask hopper-disassembler` when Hopper was not in a
standard location. Current Homebrew rejects that option combination, but
any brew command naming a cask first fetches the cask API, so each doctor
run wrote about 600 files (36 MB) into the user's Homebrew cache.

Ask brew only for its Caskroom, which brew answers without loading the
cask API, and read the application directory Homebrew recorded in the
Hopper cask's install receipt.

Fixes #1165

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 03:28:25 +00:00
Kaoru0822andGitHub 4fb2f3e6ed Merge pull request #1177 from morluto/fix/uninstall-preflight-client-config
fix(setup): stop uninstall before removals when a client config is unsafe
2026-10-08 20:22:34 -07:00
Kaoru0822andGitHub d8e314cb24 Merge pull request #1176 from morluto/fix/session-availability-active-target
fix(session): require an open target for session-bound native and artifact tools
2026-10-08 20:22:23 -07:00
N0zoM1z0 19c52f5e3f feat(website): lead with the playable dinosaur example 2026-10-09 11:21:18 +08:00
Kaoru0822andClaude Opus 5.5 b8312fa8d1 fix(server): report 2025-era client identity and features in binary_session
binary_session read the client's identity, protocol revision, and
capabilities only from the 2026-era per-request envelope. A 2025-era
connection sends none, so every such client was reported as `client: null`
with no negotiated protocol and no features, although it declared them in
`initialize`.

When a request carries no envelope, read the values the SDK keeps from the
connection's initialize handshake. An envelope still takes precedence.

Fixes #1167

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 03:20:43 +00:00
Kaoru0822andClaude Opus 5.5 fd975c75fa fix(setup): stop remaining uninstall removals after a client fails
A configuration that passes the preflight can still fail when uninstall
rereads it to edit, for example when it changes concurrently. Rereading
keeps that edit from overwriting the newer file; stopping at the failure
keeps later clients, the skill, and REA data from being removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 03:17:11 +00:00
Kaoru0822andClaude Opus 5.5 e9c48a61e4 fix(setup): keep Codex TOML comments when adding or removing REA
Setup and uninstall rewrote the whole Codex config.toml with a fresh TOML
serializer, which dropped every comment and respelled unrelated values,
although the plan promises to preserve unrelated configuration. Grok Build
already edited only REA's server tables.

Share that table editor with Codex, without Grok's root
disabled_mcp_servers handling, keep the original final line break, and
fall back to the full rewrite only when the edited text would not parse to
the requested document.

Fixes #1163

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 03:15:18 +00:00
b4927a0a4f fix(cli): advertise defaulted inputs as optional in --schema and help (#1175)
Incur derives `--schema`, the `--llms` manifests, and help synopses from
Zod's output view, where a defaulted field always has a value, so every
defaulted argument and option was listed as required even though the CLI
accepts its omission. Wrap each field that fills an omitted value as
optional after registration, keeping its default, constraints, metadata,
and parsed value.

Fixes #1169

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:11:11 +08:00
Kaoru0822andClaude Opus 5.5 5cc841d5c1 fix(setup): stop uninstall before removals when a client config is unsafe
The installation guide says malformed client configuration stops
`rea uninstall`, but uninstall reported that client as failed and still
removed other clients' registrations, the bundled skill, and with
--purge-data REA's cache and state.

Inspect every client configuration first, using the conditions setup
already treats as blocking (unsafe path, unreadable file, malformed
document), and stop before any removal when one fails. Purge paths that are
symbolic links are still retained and reported per item, as the guide now
states.

Fixes #1164

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 03:07:40 +00:00
N0zoM1z0 11b9f0346c feat(website): explain REA with Calculator and a playable dinosaur lab 2026-10-09 11:06:05 +08:00
Kaoru0822andClaude Opus 5.5 cf77d073fc fix(session): require an open target for session-bound native and artifact tools
binary_session reported native and artifact operations available whenever
their provider declared them, although these operations run on the active
target: in a closed session every call failed with target_unavailable, and
Mach-O-only operations stayed available with a ZIP or ELF target open.

Report target_required for every session-bound contract kind in a closed
session, and target_unsupported for Mach-O-only operations unless the open
target is a Mach-O executable. A host limitation is reported before target
selection, since opening a target cannot fix it; composed operations keep
deciding from their requirements.

Fixes #1166

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 03:02:40 +00:00
Kaoru0822andGitHub de5bfec79f Merge pull request #1172 from morluto/fix/macho-arm64e-variant-slices
fix(artifacts): match lipo arm64e variant names to their FAT slices
2026-10-08 19:42:05 -07:00
Kaoru0822andGitHub 37025cc09f Merge pull request #1171 from morluto/fix/extraction-preflight-cancellation
fix(artifacts): keep cancellation and the caller path in extraction refusals
2026-10-08 19:38:56 -07:00
Kaoru0822andClaude Opus 5.5 0b32577a90 fix(artifacts): match lipo arm64e variant names to their FAT slices
Newer lipo names arm64e ABI variants `arm64e.<variant>`: `arm64e.v1` for
pointer-auth ABI version 1 on freshly built universal binaries, and
`arm64e.x1` on recent macOS system binaries such as /bin/ls. The
structural FAT parser names the same slice `arm64e`, so the exact name
comparison failed and inspect_artifact reported a valid, unmodified
universal binary as an integrity mismatch ("Artifact is invalid or has
changed").

Accept `arm64e.<variant>` for an `arm64e` FAT slice. Offset, size, CPU
type, subtype (including the pointer-auth version bits when lipo prints
them) and alignment are still compared field by field. The slice keeps
lipo's name in its logical path.

Fixes #1168

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 02:37:03 +00:00
Kaoru0822andClaude Opus 5.5 eacf8461da fix(artifacts): keep cancellation and the caller path in extraction refusals
Two follow-ups to #1161 from its post-merge review:

- The new format preflight ran before scanArtifactInventory, which was
  the first cancellation check. An already-cancelled extraction of a
  DMG or PKG therefore reported unsupported_target instead of
  cancellation. Check the signal before the preflight.
- The unsupported-format and encrypted-entry refusals reported the
  realpath. When the caller selected the target through a symlink,
  junction, or other alias, the diagnostic named a different path.
  Report the caller's input path; I/O still uses the canonical path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 02:33:16 +00:00
Kaoru0822andGitHub b34bdcdb28 Merge pull request #1161 from morluto/fix/artifact-unsupported-target-errors
fix(artifacts): report wrong target kinds as unsupported targets
2026-10-08 19:30:54 -07:00
Kaoru0822andClaude Opus 5.5 de43da71ae fix(artifacts): report encrypted archive entries as unsupported extraction
extract_artifact on a ZIP containing a password-protected entry failed
the whole extraction with "Artifact is invalid or has changed. Get a
fresh copy" and named only an opaque occurrence ID. The archive was
intact; REA does not decrypt entries, so complete extraction is
unsupported for this target.

Raise AnalysisUnsupportedTargetError naming the encrypted entry's path.
Other unextractable occurrences keep their format failure but now name
the logical path alongside the occurrence ID.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 02:23:44 +00:00
Kaoru0822andGitHub 6a1e558db3 Merge pull request #1159 from morluto/fix/dylib-resolution-root-input-errors
fix(apple): report unusable dylib-resolution roots as invalid input
2026-10-08 19:23:22 -07:00
Kaoru0822andClaude Opus 5.5 8e74c9c62f fix(artifacts): refuse unsupported extraction formats before scanning
extract_artifact on a format without an extraction reader, such as a
DMG, first ran the full inventory scan (mounting the image) and then
failed with an `unavailable` artifact failure: "Check artifact support
and required tools". The target format, not the host, is unsupported.

Check the format before any inventory or output work and raise
AnalysisUnsupportedTargetError, which the provider now keeps, as it does
for the other target-kind checks in this PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 02:21:22 +00:00
Kaoru0822andClaude Opus 5.5 44d5b4b25e test(artifacts): assert asset-catalog target refusal only on macOS
Outside Darwin the session reports inspect_asset_catalog as unavailable
on the host before the provider checks the target kind, so the Linux
test shards would see capability_unavailable rather than
unsupported_target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 02:21:21 +00:00
Kaoru0822andClaude Opus 5.5 637bcb5e41 fix(artifacts): report wrong target kinds as unsupported targets
decode_interface_builder, inspect_asset_catalog, inspect_keyed_archive
and trace_dylib_resolution refused a target of the wrong kind (for
example a plain Mach-O for the asset catalog, or a plist for dyld
tracing) as an `unavailable` artifact failure: "Artifact processing is
unavailable for the current target or host. Check artifact support and
required tools." Nothing was missing on the host; the operation does not
support that kind of target.

Raise AnalysisUnsupportedTargetError for these checks and let
translateFailure keep it. The result is now `unsupported_target`, naming
the operation, the target path and the requirement, with advice to pick
a supported target or another operation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 02:16:57 +00:00
YsopwnableandGitHub 33d5ae0b52 fix(hopper): reserve Linux demo application before launch (#1158)
* test(hopper): give locals oracle explicit stack storage

* fix(hopper): reserve Linux demo application before launch

* fix(hopper): serialize stale lease recovery
2026-10-09 10:14:25 +08:00
Kaoru0822andClaude Opus 5.5 24a30fd91c fix(apple): report unusable dylib-resolution roots as invalid input
trace_dylib_resolution blamed the artifact for the caller's root
selection. A missing root, a directory, or a symlink leading outside the
bundle got "Artifact contains an unsafe or conflicting internal path ...
correct the artifact". A non-Mach-O file such as Contents/Info.plist got
"Artifact is invalid or has changed. Get a fresh copy". Any schema
failure, including a bad architecture, was reported as "roots must be
normalized paths".

Report a caller-selected root that cannot be traced as an
invalid_request issue on roots[i] with the failed constraint, and
project schema failures through their real issues. The normalized-path
rule now carries a readable message. Roots REA derives itself (bundle
executables or the active target) keep reporting artifact failures.
This matches what #1070 did for inspect_keyed_archive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 02:12:58 +00:00
b620e2a2bd fix(javascript): omit self-referential static references with disclosure (#1157)
* fix(javascript): omit self-referential static references with disclosure

A bundled module that requires its own key (`r(50)` inside webpack
module 50) resolved, through the static-reference composer, to its own
module node. The resulting self-referential imports edge failed result
validation and lost the entire analysis ("Result schema rejected 1
issue ... (custom)"). This is the same invariant as #1146; #1152 fixed
the bundler async-chunk composer, and this is the remaining adjacent
composer.

Skip a reference whose resolved target is its own source, count it, and
disclose the omission the way #1152 discloses async-chunk references.
Sibling requires are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(javascript): share the self-reference omission disclosure

The self-import (#1130), async-chunk (#1152) and static-reference
disclosures repeated one sentence template in three composers, and the
new test repeated the analyze-and-check sequence of its neighbour. That
put jscpd at its 2.5% threshold. Share the template through
selfReferenceOmissions with identical wording, and share one test helper
for the graph-wide self-reference invariants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:01:59 +08:00
58a91f03d7 fix(artifacts): recheck failed DMG detaches before reporting cleanup failure (#1155)
* fix(artifacts): recheck failed DMG detaches before reporting cleanup failure

NativeDmgArtifactReader.close() detached each device once and, when a
detach failed, asked `hdiutil info` once whether the device was still
attached. On APFS images, detaching the synthesized container ejects the
image disk too. `hdiutil info` can still list that ejected device
briefly, and a just-read volume can report busy. Either way a real
cleanup succeeded but close() reported "DMG detach or mount-root cleanup
failed" and kept the mount root.

macOS CI hit this intermittently in verify:macos-bundle: `hdiutil
detach /dev/disk23` failed with "No such file or directory" (already
ejected) and the device was still counted as attached. The #1103
review also raised the missing recheck.

After the first pass, recheck the remaining devices up to three times
with 250/500/1000 ms settle delays. Drop any that hdiutil no longer
lists, retry the detach of any still listed, and fail only for devices
that stay attached. The host seam gains an optional delay so tests run
without real waits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(artifacts): share DMG detach fixtures to stay under the duplication gate

The new busy-device and recheck cases copied the fake hdiutil host and
the APFS close sequence from neighbouring tests, which pushed jscpd to
its 2.5% threshold in Static checks. Share one single-device host
between the busy and retained-attachment cases and one APFS close
helper between the ejected-device cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:01:52 +08:00
N0zoM1z0 5160d9b717 feat(website): explain REA through a first investigation 2026-10-09 10:00:22 +08:00
089bbf58f3 fix(cli): cancel routed JavaScript analysis in rea analyze (#1156)
`rea analyze PATH` routes a JavaScript directory or ASAR to the same
static analysis as `rea analyze-javascript-application`, but it passed
no signal and was not wrapped in withCommandCancellation. On SIGINT the
process was killed with no output (signal SIGINT, empty stdout).
The dedicated command returns a typed `cancelled` result and exits 130.
The #1098 review raised this route.

Decide the route first. A JavaScript target now runs exactly like the
dedicated command: cancellation wraps logging, so the cancelled exit
code is not replaced by the logged failure status. The binary route is
unchanged and keeps its own SIGINT handling in runDirectAnalysis.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 09:48:10 +08:00
YsopwnableandGitHub 72201668ed fix(javascript): preserve graphs with async chunk self-references (#1152) 2026-10-09 09:37:37 +08:00
Kaoru0822andGitHub 84094af54a Merge pull request #1151 from morluto/docs/readme-french
docs: add French README translation
2026-10-08 18:34:31 -07:00
YsopwnableandGitHub 16624f1324 fix(evidence): cancel exports before atomic publication 2026-10-09 09:29:22 +08:00
Kaoru0822andClaude Opus 5.5 9664b2b101 docs: add French README translation
Adds README_fr.md, translated from the current English README with the
same commands, links, heading structure and FAQ entries. Links Français
from every README language selector and registers it in the README
consistency matrix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 01:21:53 +00:00
morluto 2609352eef chore(release): preserve concurrent main updates 2026-10-09 09:17:44 +08:00
morluto 90f532feb1 chore(release): sync published 6.1.0 checkpoint 2026-10-09 09:16:40 +08:00
YsopwnableandGitHub e9ebfb94ef fix(android): validate engine archive prerequisites (#1145)
* fix(android): validate engine archive prerequisites

* fix(android): bound engine metadata reads before allocation
2026-10-09 09:15:33 +08:00
Andrew BarnesandGitHub 13935077ba fix(browser): report Windows HAR heap exhaustion (#1147)
Recognize exit code 134 without a signal when the HAR decoder reports
V8 heap exhaustion. Preserve the observed termination in the memory
failure and cover Windows, POSIX, and unrelated decoder failures.
2026-10-09 09:13:00 +08:00
YsopwnableandGitHub 441813c070 perf(mcp): share repeated output schema definitions 2026-10-09 08:46:59 +08:00
morluto c12bad7f13 fix(release): publish reviewed release PR merges 2026-10-09 08:44:47 +08:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>morluto
70e5c24718 chore(main): release rea-agents 6.1.0 (#1106)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-09 08:15:17 +08:00
900 changed files with 36879 additions and 11777 deletions
+2 -3
View File
@@ -102,7 +102,7 @@ jobs:
with:
name: rea-dist
path: dist
- name: Restore generated outputs from downloaded build
- name: Generate test artifacts from the downloaded runtime
run: |
node scripts/generate-mcp-tool-catalog.mjs
node scripts/generate-skill-metadata.mjs
@@ -334,7 +334,7 @@ jobs:
- name: Process filesystem snapshot descriptor regressions
run: npm run test:focused -- src/process/capture/FilesystemSnapshot.test.ts
- name: Native Darwin ownership and process cleanup regressions
run: npm run test:focused -- tests/boundary/process/darwinProcessRunTokenReader.test.ts tests/boundary/process/processCaptureLifecycle.test.ts tests/boundary/process/processCli.test.ts tests/boundary/mcp/sessionMcp.test.ts
run: npm run test:focused -- tests/boundary/process/darwinProcessRunTokenReader.test.ts tests/boundary/process/directAnalysisSignals.test.ts tests/boundary/process/processCaptureLifecycle.test.ts tests/boundary/process/processCli.test.ts tests/boundary/mcp/sessionMcp.test.ts
reconstruction-readiness:
name: Reconstruction readiness
@@ -450,7 +450,6 @@ jobs:
tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts
tests/boundary/filesystem/setupUnavailableProvider.test.ts
tests/boundary/filesystem/windowsNpxRegistration.test.ts
src/application/ClientRegistrationIdentity.test.ts
tests/boundary/filesystem/webScriptManifestPaths.test.ts
tests/boundary/process/ProcessEvidence.host.test.ts
tests/acceptance/webModuleCli.test.ts
+122 -1
View File
@@ -2,6 +2,15 @@ name: Publish REA website
on:
workflow_dispatch:
inputs:
target:
description: Publish both hosts, or Pages while configuring Cloudflare
type: choice
required: true
default: both
options:
- both
- pages
permissions:
contents: read
@@ -19,12 +28,42 @@ jobs:
contents: read
pages: read
steps:
- name: Validate the publication target
env:
PUBLISH_TARGET: ${{ inputs.target }}
run: |
case "$PUBLISH_TARGET" in
both|pages) ;;
*) echo "::error::Unknown publication target: $PUBLISH_TARGET"; exit 1 ;;
esac
- name: Require Cloudflare publishing credentials
if: inputs.target == 'both'
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
if [[ -z "$CLOUDFLARE_API_TOKEN" || -z "$CLOUDFLARE_ACCOUNT_ID" ]]; then
echo "::error::Set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID in repository Actions secrets before publishing."
exit 1
fi
- name: Check out the repository
uses: actions/checkout@v4
- name: Prepare the example download
run: python3 scripts/prepare-website.py
- name: Check website links and publishing configuration
run: python3 scripts/verify-website.py
- name: Record the published commit
run: |
python3 - <<'PY'
import json
import os
from pathlib import Path
Path("website/public/deployment.json").write_text(
json.dumps({"commit": os.environ["GITHUB_SHA"]}) + "\n",
encoding="utf-8",
)
PY
- name: Configure Pages
uses: actions/configure-pages@v5
- name: Upload the website
@@ -35,14 +74,96 @@ jobs:
deploy:
needs: prepare
runs-on: ubuntu-latest
timeout-minutes: 5
timeout-minutes: 15
permissions:
contents: read
actions: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Check out publishing configuration
if: inputs.target == 'both'
uses: actions/checkout@v4
- name: Set up Node.js for Wrangler
if: inputs.target == 'both'
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
- name: Download the verified website
if: inputs.target == 'both'
uses: actions/download-artifact@v4
with:
name: github-pages
path: ${{ runner.temp }}/website-archive
- name: Extract the shared publishing artifact
if: inputs.target == 'both'
run: |
mkdir -p "$RUNNER_TEMP/rea-website-public"
tar -xf "$RUNNER_TEMP/website-archive/artifact.tar" -C "$RUNNER_TEMP/rea-website-public"
- name: Publish the website to Cloudflare
if: inputs.target == 'both'
uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
wranglerVersion: "4.149.0"
packageManager: npm
command: deploy --cwd website --env production --assets "${{ runner.temp }}/rea-website-public"
- name: Publish to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
- name: Verify the selected published revisions
env:
PAGES_URL: ${{ steps.deployment.outputs.page_url }}
PUBLISH_TARGET: ${{ inputs.target }}
run: |
python3 -u - <<'PY'
import json
import os
import sys
import time
from urllib.error import URLError
from urllib.parse import urljoin
from urllib.request import Request, urlopen
expected = os.environ["GITHUB_SHA"]
pages_url = os.environ["PAGES_URL"]
if not pages_url:
sys.exit("GitHub Pages returned no published URL.")
target = os.environ["PUBLISH_TARGET"]
if target not in ("both", "pages"):
sys.exit("Unknown publication target: " + target)
hosts = {"GitHub Pages": pages_url}
if target == "both":
hosts["Cloudflare"] = "https://rea.tools/"
pending = dict(hosts)
for attempt in range(24):
for name, base in list(pending.items()):
url = urljoin(base, "deployment.json") + "?commit=" + expected
try:
request = Request(url, headers={
"Cache-Control": "no-cache",
"User-Agent": "REA-Website-Verifier/1.0 (+https://github.com/morluto/rea)",
})
with urlopen(request, timeout=10) as response:
actual = json.load(response).get("commit")
if actual == expected:
print(f"{name} serves {expected}: {base}")
del pending[name]
else:
print(f"Waiting for {name}: observed revision {actual}")
except (URLError, TimeoutError, ValueError) as error:
print(f"Waiting for {name}: {error}")
if not pending:
break
if attempt < 23:
time.sleep(5)
if pending:
sys.exit("Could not verify published revision on: " + ", ".join(pending))
with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as summary:
links = " and ".join(f"[{name}]({url})" for name, url in hosts.items())
summary.write(f"Published commit `{expected}` to {links}.\n")
PY
+2 -4
View File
@@ -49,6 +49,7 @@ Thumbs.db
# Generated website download
/website/public/examples/notes-example.zip
/website/public/deployment.json
# Local Cloudflare development state
/website/.wrangler/
@@ -57,11 +58,8 @@ Thumbs.db
!/website/.dev.vars.example
!/website/.env*.example
# Build-generated MCP tool catalog (regenerated by `npm run build`; never commit)
src/generatedMcpToolCatalog.ts
# Derived catalogs, portable conformance projections, and packaged skill bundles
# (regenerated from this checkout by `npm run build:cached`; never commit)
# (regenerated by `npm run docs:generate`; never commit)
/skills/
/docs/public/product-catalog.json
/docs/verification/managed-conformance-manifest.json
+9 -5
View File
@@ -24,15 +24,15 @@ REA is a layered ESM TypeScript application. Dependencies flow inward from pure
- `src/application/` composes shared CLI/MCP workflows, including Evidence provenance, unknowns, and eligible snapshot bindings; `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. Reuse its supervision and identity primitives before adding provider-local lifecycle code; a PID and executable pathname alone do not establish ownership after exit or reuse. `bridge/` contains provider-side adapters.
- `tests/` contains unit, composition, boundary, acceptance, and conformance tests. `scripts/verify-*` and capability directories under `scripts/verify/` contain real-toolchain checks.
- `docs/public/product-catalog.json`, `docs/verification/managed-conformance-*.json`, and `skills/` are ignored build outputs. Update source contracts and authored instructions in `skill-src/`, then run `npm run build:cached`; never commit derived catalog digests or portable conformance projections.
- `src/generatedMcpToolCatalog.ts` is build-generated and gitignored. Never commit it; resolve any trace of it in merges by deleting it and running `npm run build:cached`.
- `docs/public/product-catalog.json`, `docs/verification/managed-conformance-*.json`, and `skills/` are ignored build outputs. Update source contracts and authored instructions in `skill-src/`, then run `npm run docs:generate`; never commit derived catalog digests or portable conformance projections.
- `.cache/mcp-tool-catalog.json` is generated test metadata. Generate it with `npm run mcp-catalog:generate`; source checking and runtime builds do not consume it.
## Build, Test, and Development Commands
- `npm ci`: install the locked dependencies.
- `npm run build:cached`: build the CLI and MCP server.
- `npm run build:cached`: compile the CLI/MCP runtime and bundle its authored skill. Test catalogs and documentation/conformance artifacts have separate tasks.
- `npm run test:local`: run changed source tests without building; pass exact source test paths to run them regardless of Git status.
- `npm run test:focused -- PATH...`: run exact test files; build first for boundary, acceptance, or process-global tests.
- `npm run test:focused -- PATH...`: run exact test files, preparing runtime and generated artifacts required by the selected tests.
- `npm run check:changed`: run cached static checks and source tests affected since the branch merge base (default `origin/main`).
- `npm run check:fast`: run cached typecheck and lint checks.
- `npm run check:pr`: opt into the complete local deterministic gate and generated-document checks for broad changes; CI owns full coverage. Routine iterations need focused tests and relevant checks, not the whole gate each time.
@@ -50,6 +50,8 @@ Configuration is parsed and validated by `src/config.ts` and `src/config/`. Keep
Use ESM TypeScript, two-space indentation, and the committed Oxfmt configuration. Keep compiler strictness intact (`strict`, `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`, `verbatimModuleSyntax`). Use `camelCase` for values/functions, `PascalCase` for classes/types, and `UPPER_SNAKE_CASE` for constants. Parse unknown values at every MCP, environment, and subprocess boundary. Avoid `any`, unchecked casts, non-null assertions, import-time I/O, and floating promises. Exported APIs require concise JSDoc. Model expected failures with the tagged error algebra and `Result`, not broad exception wrappers.
Assess brittleness during implementation, debugging, review, and maintenance. Simplify duplicated rules, scattered defaults, caller-specific exceptions, and unclear invariants in touched code. Prefer standard format-aware parsers and existing shared helpers; normalize once at the owning boundary while preserving source evidence. Keep refactors focused on the task, and introduce abstractions only when they remove observed complexity. Verify preserved behavior and corrected edge cases through affected callers.
## Boundary Contracts
Treat a boundary as a contract between the producer's actual representation and the consumer's required meaning. When implementing or auditing a boundary, trace the value through parsing, normalization, authorization, serialization, and the CLI/MCP result. Establish affected callers from their code paths; similar tool names or workflows do not prove that they share a schema or failure mode.
@@ -92,7 +94,9 @@ See [docs/tool-design.md](docs/tool-design.md) for the design checklist. When us
## Testing Guidelines
Prioritize real end-to-end workflows, boundary integration, and golden producer data. Keep a focused module test when it covers a distinct failure or semantic case absent from those workflows; test paths do not establish depth. See [docs/testing.md](docs/testing.md) for pruning and classification rules.
Prefer full end-to-end workflows through the public CLI or MCP with real providers and no mocked dependencies, then integration across production boundaries, then goldens captured from real producers. Keep a focused module test only for a distinct failure or semantic case that stronger workflows cannot reliably reproduce. Delete redundant getter, serialization, enum, count, and snapshot assertions when a stronger consumer workflow already proves the claim. Test paths, compiled imports, and suites named E2E do not establish end-to-end coverage. See [docs/testing.md](docs/testing.md) for pruning and classification rules.
Treat advertised examples as executable contracts: derive them from representative producer data, exercise them through the advertised CLI or MCP workflow, and assert the behavior they claim to demonstrate. Share production projections where practical and declare omitted coverage explicitly.
Name tests `*.test.ts`. Use Vitest and production seams (`tests/fixtures/`) rather than module mocks. Domain tests assert pure behavior; adapter tests use fake launcher/socket seams; MCP tests connect with the client SDK version pinned in `package.json`. Preserve the canonical tool inventory defined by `TOOL_CONTRACTS` and verified through `CATALOG_IDENTITY` and generated product metadata. Cover malformed input, cancellation, lifecycle cleanup, and actual format, protocol, host-permission, and target-identity boundaries. Do not add tests that merely freeze arbitrary caps or prescribed call sequences. Real Hopper, Ghidra, browser, managed conformance, and any real managed-tool claims cannot be replaced by mocks; use the corresponding `verify:*` command.
+1
View File
@@ -159,6 +159,7 @@
### Performance Improvements
* **mcp:** advertise each tool JSON Schema once per target ([#1059](https://github.com/morluto/rea/issues/1059)) ([5ccb924](https://github.com/morluto/rea/commit/5ccb9241be15bb6a19b99fbbfb27ff3356ba157b))
* **build:** avoid duplicate documentation generation ([c701376](https://github.com/morluto/rea/commit/c701376777212ec9acfd10c97f45b1eaf0c885c6))
* **javascript:** index configuration and module lookups ([67252b2](https://github.com/morluto/rea/commit/67252b23b9e35e9526fed7842dab47a2c7c9fe5f))
* **javascript:** reuse validated immutable graphs ([#1112](https://github.com/morluto/rea/issues/1112)) ([ec6db0f](https://github.com/morluto/rea/commit/ec6db0f0630c4b0b35f0db84c0030784c1aa58a6))
+24 -6
View File
@@ -42,9 +42,10 @@ npm run check:fast
`npm ci` installs the exact dependencies and prepares the Husky hooks without
building the project. Run `npm run build:cached` when you need the standalone
CLI or MCP server; `npm run build` remains the uncached compiler leaf used by
Turbo. Turbo caches deterministic builds and static checks across Git
worktrees. After a package, lockfile, or managed-skill version change, run
CLI or MCP server; it compiles the runtime and bundles the authored skill.
`npm run build` forces those tasks to rerun. Neither command generates test
catalogs, documentation projections, or managed verification evidence. Turbo
caches deterministic builds and static checks across Git worktrees. After a package, lockfile, or managed-skill version change, run
`npm run metadata:generate` before building.
Keep dependencies flowing inward through the existing domain, contracts, provider, application, server, and adapter layers. Parse unknown values at process and protocol boundaries, model expected failures with `Result`, and preserve the canonical tool inventory defined by `TOOL_CONTRACTS` unless a deliberate contract change updates every verifier, generated catalog artifact, and snapshot. Keep tool discovery complete and report capability- and session-scoped availability through `binary_session`.
@@ -96,7 +97,22 @@ Packaging, setup, installation, or distribution changes also require
`npm run verify:package` and `npm pack --dry-run`. Provider behavior changes
require the matching real-provider `verify:*` lane.
`check:fast` runs cached typecheck and lint, reporting diagnostics on failure.
`check:fast` runs cached source/test typecheck and lint without compiling or
running generators. `npm run test:prepare` prepares all artifacts consumed by
the complete suite; focused tests prepare only their runtime and declared
artifact dependencies. The test-only MCP catalog lives at
`.cache/mcp-tool-catalog.json` and can be regenerated independently with
`npm run mcp-catalog:generate`.
Build and generation commands hold one SQLite exclusive transaction around
their whole Turbo graph, including cache restoration. The persistent database
in `.cache/rea-command-locks/` is a lock identity, not a stale-file marker;
the operating system releases ownership when the command supervisor exits.
On POSIX, that supervisor retains the transaction and process-group identity
through cancellation, including loss of its parent runner. Windows cancellation
waits for the command to complete. Do not remove the database while commands
are running.
`check` adds formatting, dead-code, and package-metadata freshness checks.
Formatting uses Oxfmt and the committed `.oxfmtrc.json`; generated sources use
the same configuration. Pre-commit formats and lints staged files; pre-push runs
@@ -104,8 +120,10 @@ the same configuration. Pre-commit formats and lints staged files; pre-push runs
`docs:check` builds and validates generated metadata for the current checkout.
The product catalog (`docs/public/product-catalog.json`), portable managed
conformance projections (`docs/verification/managed-conformance-*.json`), and
packaged skill (`skills/`) are ignored build outputs. Edit skill instructions
and references in `skill-src/`; the build adds catalog-dependent metadata to
packaged skill (`skills/`) are ignored generated outputs. `docs:generate`
prepares documentation and managed evidence; `evidence:generate` prepares only
the managed commitment and its runtime/skill dependencies. Edit skill
instructions and references in `skill-src/`; the skill task adds metadata to
the packaged copy without rewriting authored files. The generated manifest
commits to that exact packaged skill bundle. It is a portable projection of
the deterministic managed verifier, not a record of optional real-provider runs.
+7 -5
View File
@@ -1,6 +1,6 @@
<div align="center">
**English** · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
**English** · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
# REA: Reverse Engineer Anything
@@ -157,7 +157,7 @@ Additional tools and host support depend on the target:
| Websites | Page structure, scripts, network observations and requested screenshots | A Chrome-family browser; [browser analysis](https://rea.tools/guides/browser/) |
| Saved network captures | Requests, responses, exposed payloads and source locations | HAR; mitmdump on Linux for native mitmproxy captures; [capture guide](docs/web-network-captures.md) |
| .NET assemblies | Metadata, CIL instructions, declared native dependencies and build comparisons | Static inspection; [managed-code guide](docs/managed-code-analysis.md) |
| Android APKs | Manifest declarations, classes, decompiled methods and references | Headless JADX and a full JDK on Linux/macOS; [Android guide](docs/android-analysis.md) |
| Android APKs | Manifest declarations, classes, decompiled methods and references | Headless JADX and a full JDK on Linux/macOS/Windows x64; [Android guide](docs/android-analysis.md) |
| Firmware | Regions, extraction results and native-analysis handoffs | Binwalk / Unblob on Linux; [firmware guide](docs/firmware-analysis.md) |
| Packages and resources | File inventories, digests, plists, Apple bundle anatomy and extracted resources | [Artifact and JavaScript guide](docs/javascript-artifact-reconstruction.md), [Apple applications](docs/apple-application-analysis.md) |
| Process behavior | Terminal output, interactions, exit and filesystem observations, and run comparisons | Linux/macOS with a native PTY; [process capture](docs/process-capture.md) |
@@ -298,7 +298,7 @@ For exact options, prerequisites and result contracts:
- [Readiness and troubleshooting](docs/installation.md#check-readiness-for-your-task): diagnose one agent or analysis engine.
- [CLI and Evidence](docs/cli.md): commands, provider selection, snapshots, import/export and exit statuses.
- [MCP contracts](docs/mcp-contracts.md) and [agent prompts](docs/mcp-prompts.md): tool results, sessions and guided investigations.
- [Tool catalog](docs/mcp-contracts.md#generated-catalog): build-generated inventory of tools, providers and CLI commands.
- [Tool catalog](docs/mcp-contracts.md#generated-catalog): generated inventory of tools, providers and CLI commands.
- [Roadmap](docs/roadmap.md): planned work and capability trackers.
Report vulnerabilities through [SECURITY.md](SECURITY.md).
@@ -319,9 +319,9 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and checks,
## Star history
🎉 **20,000 GitHub stars — thank you!**
🎉 **30,000 GitHub stars — thank you!**
Thanks to everyone using REA, reporting bugs, testing builds, and contributing fixes.
Thanks to everyone using REA, reporting bugs, requesting features, testing builds, and contributing fixes.
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
@@ -335,6 +335,8 @@ Thanks to everyone using REA, reporting bugs, testing builds, and contributing f
REA provides tools for lawful reverse-engineering research, analysis, and reconstruction. You are responsible for obtaining any required authorization and complying with applicable laws. The project does not endorse illegal or unauthorized use.
REA is an open-source software project. We have not issued or endorsed any cryptocurrency or token. Tokens using the REA name are not affiliated with the project.
## License
[MIT](LICENSE)
+3 -3
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · **العربية** · [فارسی](README_fa.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · **العربية** · [فارسی](README_fa.md)
# REA: هندسة عكسية لأي شيء
@@ -266,9 +266,9 @@ npx rea-agents@latest setup
## سجل النجوم
🎉 **20,000 نجمة على GitHub — شكرًا لكم!**
🎉 **30,000 نجمة على GitHub — شكرًا لكم!**
شكرًا لكل من يستخدم REA، ويبلّغ عن الأخطاء، ويختبر إصدارات البناء، ويساهم بالإصلاحات.
شكرًا لكل من يستخدم REA، ويبلّغ عن الأخطاء، ويقترح ميزات، ويختبر إصدارات البناء، ويساهم بالإصلاحات.
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
+3 -3
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · **Deutsch** · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · **Deutsch** · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
# REA: Alles reverse-engineeren
@@ -264,9 +264,9 @@ Die Entwicklungsumgebung und Prüfungen beschreibt [CONTRIBUTING.md](CONTRIBUTIN
## Sternverlauf
🎉 **20.000 GitHub-Sterne – vielen Dank!**
🎉 **30.000 GitHub-Sterne – vielen Dank!**
Danke an alle, die REA nutzen, Fehler melden, Builds testen und Fehlerbehebungen beitragen.
Danke an alle, die REA nutzen, Fehler melden, Funktionswünsche äußern, Builds testen und Fehlerbehebungen beitragen.
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
+3 -3
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · **Español** · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · **Español** · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
# REA: Ingeniería inversa de cualquier cosa
@@ -264,9 +264,9 @@ Consulta [CONTRIBUTING.md](CONTRIBUTING.md) para preparar el entorno de desarrol
## Historial de estrellas
🎉 **20.000 estrellas en GitHub: ¡gracias!**
🎉 **30.000 estrellas en GitHub: ¡gracias!**
Gracias a quienes usan REA, informan de errores, prueban compilaciones y contribuyen con correcciones.
Gracias a quienes usan REA, informan de errores, solicitan funciones, prueban compilaciones y contribuyen con correcciones.
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
+3 -3
View File
@@ -1,6 +1,6 @@
<div align="center" dir="rtl">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · **فارسی**
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · **فارسی**
# REA: مهندسی معکوس هر چیزی
@@ -249,9 +249,9 @@ npx rea-agents@latest setup
## تاریخچهٔ ستاره‌ها
🎉 **۲۰٬۰۰۰ ستارهٔ GitHub — سپاسگزاریم!**
🎉 **۳۰٬۰۰۰ ستارهٔ GitHub — سپاسگزاریم!**
از همهٔ کسانی که از REA استفاده می‌کنند، خطاها را گزارش می‌دهند، buildها را آزمایش می‌کنند و در بهبود پروژه مشارکت دارند سپاسگزاریم.
از همهٔ کسانی که از REA استفاده می‌کنند، خطاها را گزارش می‌دهند، قابلیت‌های جدید پیشنهاد می‌کنند، buildها را آزمایش می‌کنند و در بهبود پروژه مشارکت دارند سپاسگزاریم.
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date"><picture><source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date&amp;theme=dark&amp;legend=top-left" /><source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date" /><img alt="تاریخچهٔ ستاره‌های GitHub پروژهٔ REA" src="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date" /></picture></a>
+286
View File
@@ -0,0 +1,286 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · **Français** · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
# REA : rétro-ingénierie de tout
### Un seul MCP pour la rétro-ingénierie des binaires, des applications et du comportement à l'exécution.
**Repérez une fonctionnalité qui vous plaît. Comprenez son fonctionnement, jusqu'au niveau du binaire.**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js 22+](https://img.shields.io/badge/Node.js-22.19%2B-339933?style=flat-square&logo=nodedotjs&logoColor=white)](https://nodejs.org/)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
[![MIT license](https://img.shields.io/badge/license-MIT-f4c430?style=flat-square)](LICENSE)
[![Discord](https://img.shields.io/discord/1556595354999332884?logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/GkcryMnJDM)
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
**[Site web](https://rea.tools/) · [Guides](https://rea.tools/guides/) · [Études de cas](https://rea.tools/showcase/)**
[Démarrage rapide](#démarrage-rapide) · [Comment fonctionne REA](#comment-fonctionne-rea) · [Ce que vous pouvez analyser](#ce-que-vous-pouvez-analyser) · [Études de cas](#études-de-cas) · [Questions fréquentes](#questions-fréquentes) · [Documentation](#documentation)
<code>npx rea-agents setup</code>
<br />
<img src="docs/assets/rea-hopper-analysis.png" alt="REA lance son pont d'analyse dans Hopper pendant l'inspection d'un binaire natif" width="1200" />
<br />
<table aria-label="REA community">
<tr>
<td align="center" width="360">
<a href="https://discord.gg/GkcryMnJDM">
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
<strong>Rejoignez la communauté de rétro-ingénierie</strong>
</a><br />
<sub>Discord · Questions et réponses · Partage de projets</sub>
</td>
</tr>
</table>
<br />
</div>
---
Vous avez repéré dans une application une fonctionnalité que vous aimeriez avoir dans votre propre produit ? Demandez à votre agent de l'étudier avec REA. Il peut inspecter l'application sans son code source, expliquer le fonctionnement de la fonctionnalité, présenter les preuves et en construire une version pour votre projet.
REA connecte votre agent à des outils d'inspection de binaires natifs, d'applications JavaScript et Electron, d'assemblys .NET et de sites web. Les mêmes outils sont aussi disponibles depuis votre terminal. L'analyse s'exécute localement, et chaque conclusion s'accompagne des preuves et des limites sur lesquelles elle repose.
Setup enregistre REA auprès de votre agent et installe les instructions de workflow correspondantes. L'analyse native peut utiliser une installation existante de Hopper ou de Ghidra ; Setup peut aussi installer Hopper après votre accord. L'analyse statique JavaScript ne nécessite aucun de ces moteurs.
> **[Visitez le site web de REA](https://rea.tools/)** pour les instructions d'installation, des guides illustrés et des études de cas réelles.
## Démarrage rapide
### Configurer votre agent
Une fois Node.js et npm installés, exécutez :
```bash
npx rea-agents setup
```
Choisissez vos agents, vérifiez les modifications proposées et approuvez-les. Setup ajoute le serveur MCP de REA et les instructions de workflow correspondantes, et sauvegarde la configuration existante. Redémarrez ensuite votre agent.
Setup prend en charge Claude Code, Codex, Cursor, Gemini CLI, Grok Build et [d'autres agents](docs/installation.md#supported-agents). Consultez [Installation et configuration](docs/installation.md) pour la configuration des fournisseurs et l'enregistrement MCP manuel.
### Interroger votre agent
```text
Comprends comment fonctionne la recherche dans l'app Notes, montre-moi les preuves
et construis une fonctionnalité similaire pour mon projet.
```
Remplacez Notes par votre application cible et la fonctionnalité que vous voulez comprendre.
### Utiliser le terminal
Inspectez le répertoire extrait ou l'archive ASAR d'une application JavaScript/Electron :
```bash
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
```
Le résultat comprend les modules, les imports, les frontières Electron et les preuves associées. Remplacez le chemin par votre cible, par exemple `"D:/apps/example"` sous Windows.
Pour installer la commande `rea` pour un usage régulier :
```bash
npm install --global rea-agents
rea --help
```
Pour l'analyse native, configurez d'abord un fournisseur. Consultez le [guide CLI et Evidence](docs/cli.md) pour les commandes natives, le choix du fournisseur, les snapshots et l'utilisation dans des scripts.
### Mettre à jour REA
REA évolue rapidement, et les nouvelles versions apportent fréquemment des corrections de bugs. Maintenez votre installation à jour.
Pour une CLI installée avec npm :
```bash
rea update
```
Pour actualiser les enregistrements de vos agents et le skill, exécutez la commande setup affichée par la mise à jour.
Si vous utilisez `npx`, mettez à jour la configuration de votre agent avec :
```bash
npx rea-agents@latest setup
```
Vérifiez les modifications proposées par Setup et redémarrez votre agent. Pour des commandes CLI ponctuelles, utilisez `npx rea-agents@latest` suivi de la commande.
## Comment fonctionne REA
Votre agent appelle REA via MCP pour inspecter la cible et suivre le code pertinent. REA renvoie ses constatations avec les preuves correspondantes. L'agent s'en sert pour poser des questions complémentaires, expliquer le comportement, ou écrire et tester une implémentation. Les commandes CLI utilisent les mêmes workflows.
![Flux d'investigation de REA : votre agent pose une question sur une cible locale, REA l'inspecte et la trace avec des outils d'analyse, et l'agent utilise le code, les références et les inconnues renvoyés pour expliquer, implémenter et tester.](website/public/assets/figures/rea-investigation-flow.svg)
[Ouvrir la figure en taille réelle](website/public/assets/figures/rea-investigation-flow.svg).
<a id="current-status"></a>
## Ce que vous pouvez analyser
REA nécessite Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, ainsi que npm. Les outils supplémentaires et les hôtes pris en charge dépendent de la cible :
| Cible | Ce que REA renvoie | Prérequis et guide |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Binaires natifs | Pseudo-code, assembleur, chaînes, symboles, appels et références | Hopper, Ghidra ou IDA ; [analyse native](https://rea.tools/guides/native/) |
| Structure ELF hors ligne | Sections, segments, symboles et relocations d'origine, et protections statiques candidates | pwntools fourni par l'utilisateur sous Linux x64 ; [diagnostics binaires](docs/binary-diagnostics.md) |
| Bytecode EVM | Sélecteurs de dispatch, offsets en octets, arguments inférés et mutabilité | Fichier local brut ou hexadécimal ; [guide EVM hors ligne](docs/evm-bytecode.md) |
| Crashs Linux enregistrés | Notes brutes, registres et signaux de chaque thread enregistré, et correspondances de mappings facultatives | pwntools fourni par l'utilisateur ; GDB/pwndbg en option ; [crashs enregistrés](docs/recorded-crashes.md) |
| JavaScript / Electron | Modules, imports, source maps, routes, IPC et relations avec les modules natifs | Node.js et npm ; [analyse d'applications](https://rea.tools/guides/javascript/) |
| Sites web | Structure des pages, scripts, observations réseau et captures d'écran demandées | Un navigateur de la famille Chrome ; [analyse du navigateur](https://rea.tools/guides/browser/) |
| Captures réseau enregistrées | Requêtes, réponses, payloads exposés et emplacements dans le code source | HAR ; mitmdump sous Linux pour les captures mitmproxy natives ; [guide des captures](docs/web-network-captures.md) |
| Assemblys .NET | Métadonnées, instructions CIL, dépendances natives déclarées et comparaisons de builds | Inspection statique ; [guide du code managé](docs/managed-code-analysis.md) |
| APK Android | Déclarations du manifeste, classes, méthodes décompilées et références | JADX headless et un JDK complet sous Linux/macOS ; [guide Android](docs/android-analysis.md) |
| Firmware | Régions, résultats d'extraction et passage de relais vers l'analyse native | Binwalk / Unblob sous Linux ; [guide firmware](docs/firmware-analysis.md) |
| Paquets et ressources | Inventaires de fichiers, empreintes, plists, anatomie des bundles Apple et ressources extraites | [Guide artefacts et JavaScript](docs/javascript-artifact-reconstruction.md), [applications Apple](docs/apple-application-analysis.md) |
| Comportement des processus | Sortie du terminal, interactions, code de sortie, observations du système de fichiers et comparaisons | Linux/macOS avec un PTY natif ; [capture de processus](docs/process-capture.md) |
L'inspection statique JavaScript et .NET lit les fichiers fournis sans exécuter l'application. La capture à l'exécution lance la cible choisie ou interagit avec elle avec vos permissions utilisateur ; chaque guide d'exécution décrit ses effets.
<a id="choosing-a-deep-analysis-provider"></a>
Les formats natifs et les hôtes pris en charge varient selon le fournisseur. Consultez [la configuration de Hopper et Ghidra](docs/installation.md#hopper), le [guide IDA](docs/ida-provider.md) et [la prise en charge expérimentale de Ghidra sous Windows](docs/windows-ghidra-p0.md). Ghidra prend aussi en charge [l'analyse DOS 16 bits](docs/ghidra-dos.md). Pour les gros binaires, augmentez son délai de démarrage avec `REA_GHIDRA_STARTUP_TIMEOUT_MS`. Pour le choix du fournisseur, consultez le [guide CLI](docs/cli.md#choose-a-provider). Vérifiez la [disponibilité dans les versions publiées](docs/installation.md#released-package-and-main) pour les fonctionnalités ajoutées depuis la dernière version npm.
## Études de cas
### DX-Ball : reconstruire un calcul de panoramique sonore
Suivez un appel sonore jusqu'à la fonction qui convertit une position en panoramique, inspectez ses instructions et transformez un pseudo-code incomplet en C. La reconstruction réussit 3 205 cas issus du x86 d'origine et reproduit à l'identique les 63 octets de la fonction compilée.
[Lire l'étude de cas](https://rea.tools/showcase/dx-ball/) · [Dépôt de la reconstruction](https://github.com/N0zoM1z0/dx-ball)
### Notion : suivre le pont du presse-papiers d'Electron
Trouvez l'API de presse-papiers du renderer, suivez-la à travers le preload et l'IPC jusqu'au processus principal, puis inspectez le format de presse-papiers enrichi.
[Lire l'étude de cas](https://rea.tools/showcase/notion/)
### TH04 : retrouver un calcul d'anneau de projectiles sous DOS
Inspectez les instructions 16 bits du jeu PC-98 d'origine, retrouvez les calculs d'angles fixes et visés, et comparez le C++ reconstruit avec la sortie du compilateur d'époque.
[Lire l'étude de cas](https://rea.tools/showcase/th04/) · [Dépôt de la reconstruction](https://github.com/N0zoM1z0/th04)
Si vous avez utilisé REA sur un projet intéressant, nous serions ravis de le découvrir. Partagez votre cas dans une [issue](https://github.com/morluto/rea/issues) ou une [pull request](https://github.com/morluto/rea/pulls), en précisant la cible, votre question, la manière dont REA vous a aidé et ce que vous avez découvert.
## Questions fréquentes
<details>
<summary><strong>Quels agents peuvent utiliser REA ?</strong></summary>
Tout agent compatible avec les serveurs MCP locaux. Setup configure les [agents pris en charge](docs/installation.md#supported-agents) ; les autres clients peuvent utiliser [l'enregistrement MCP manuel](docs/installation.md#mcp-registry).
</details>
<details>
<summary><strong>Ai-je besoin de Hopper, Ghidra ou IDA ?</strong></summary>
L'analyse native approfondie utilise l'un d'eux. L'inspection statique JavaScript et .NET fonctionne sans moteur d'analyse native. Setup peut installer Hopper après votre accord ; Ghidra et IDA utilisent vos installations existantes. Consultez [la configuration des fournisseurs](docs/installation.md#hopper).
</details>
<details>
<summary><strong>Dois-je lancer Hopper au préalable ?</strong></summary>
REA lance Hopper lorsqu'une opération en a besoin. Sous macOS, une boîte de dialogue de premier lancement peut vous demander de choisir le mode démo ou d'activer votre licence. Consultez [Démarrage et dépannage de Hopper](docs/installation.md#launcher-paths-and-troubleshooting).
</details>
<details>
<summary><strong>Que fait l'installation du skill depuis skills.sh ?</strong></summary>
Le skill fournit des instructions d'investigation à votre agent. Utilisez `rea setup` pour enregistrer le serveur MCP de REA et installer les instructions correspondantes, puis redémarrez votre agent. Consultez [l'installation du skill seul](docs/installation.md#skill-only-installation).
</details>
<details>
<summary><strong>Quel code REA renvoie-t-il ?</strong></summary>
L'analyse native renvoie du pseudo-code et de l'assembleur. L'analyse JavaScript/Electron récupère les modules et leurs relations. Votre agent s'appuie sur ces résultats pour écrire et tester une implémentation ; les [études de cas](#études-de-cas) en donnent des exemples concrets.
</details>
<details>
<summary><strong>REA envoie-t-il mon application en ligne ?</strong></summary>
REA analyse les cibles localement. Votre agent reçoit les résultats des outils, et son fournisseur de modèle applique sa propre politique de données.
</details>
<details>
<summary><strong>Que faire si je rencontre un bug ?</strong></summary>
Commencez par mettre à jour ; une version récente l'a peut-être déjà corrigé.
Pour une CLI installée avec npm :
```bash
rea update
```
Pour une configuration d'agent via `npx` :
```bash
npx rea-agents@latest setup
```
Si vous utilisez un agent, terminez l'[actualisation de la configuration](#mettre-à-jour-rea) et redémarrez-le. Relancez la même tâche. Si le problème persiste, [ouvrez une issue](https://github.com/morluto/rea/issues) en indiquant votre version de REA, le type de cible, les étapes pour reproduire le problème et la sortie d'erreur.
</details>
## Documentation
Commencez par les [guides pas à pas](https://rea.tools/guides/) du site web. Pour les options exactes, les prérequis et les contrats de résultats :
- [Installation et configuration](docs/installation.md) : enregistrement des agents, configuration des fournisseurs, mises à jour et désinstallation.
- [Vérification et dépannage](docs/installation.md#check-readiness-for-your-task) : diagnostiquer un agent ou un moteur d'analyse précis.
- [CLI et Evidence](docs/cli.md) : commandes, choix du fournisseur, snapshots, import/export et codes de sortie.
- [Contrats MCP](docs/mcp-contracts.md) et [prompts pour agents](docs/mcp-prompts.md) : résultats des outils, sessions et investigations guidées.
- [Catalogue des outils](docs/mcp-contracts.md#generated-catalog) : inventaire des outils, fournisseurs et commandes CLI, généré lors du build.
- [Feuille de route](docs/roadmap.md) : travaux prévus et suivi des capacités.
Signalez les vulnérabilités via [SECURITY.md](SECURITY.md).
## Contribuer
Votre aide sur REA est la bienvenue ! [Ouvrez une issue](https://github.com/morluto/rea/issues) pour signaler un bug ou proposer une fonctionnalité, ou [envoyez une pull request](https://github.com/morluto/rea/pulls) pour améliorer le code ou la documentation.
Consultez [CONTRIBUTING.md](CONTRIBUTING.md) pour l'environnement de développement et les vérifications, la page [tests](docs/testing.md) pour les filières de vérification, et la [carte de l'architecture](docs/architecture.mermaid) pour la structure du projet.
## Liens du projet
[Site web](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [Sécurité](SECURITY.md)
## Historique des étoiles
🎉 **30 000 étoiles GitHub — merci !**
Merci à toutes les personnes qui utilisent REA, signalent des bugs, proposent des fonctionnalités, testent les builds et contribuent des correctifs.
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date&amp;theme=dark&amp;legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date" />
<img alt="Historique des étoiles GitHub de REA" src="https://api.star-history.com/chart?repos=morluto/rea&amp;type=date" />
</picture>
</a>
## Avertissement
REA fournit des outils destinés à la recherche, à l'analyse et à la reconstruction légales en rétro-ingénierie. Il vous appartient d'obtenir toute autorisation nécessaire et de respecter les lois applicables. Le projet n'encourage aucune utilisation illégale ou non autorisée.
## Licence
[MIT](LICENSE)
+3 -3
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · **日本語** · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · **日本語** · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
# REA:あらゆるものをリバースエンジニアリング
@@ -266,9 +266,9 @@ REA への貢献を歓迎します![issue を作成](https://github.com/morlut
## スター履歴
🎉 **GitHub スター 20,000 件、ありがとうございます!**
🎉 **GitHub スター 30,000 件、ありがとうございます!**
REA の利用、バグ報告、ビルドのテスト、修正への貢献に感謝します。
REA の利用、バグ報告、機能の要望、ビルドのテスト、修正への貢献に感謝します。
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
+3 -3
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · **한국어** · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · **한국어** · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
# REA: 무엇이든 리버스 엔지니어링
@@ -266,9 +266,9 @@ REA에 도움을 보태 주세요! [issue를 등록](https://github.com/morluto/
## 스타 기록
🎉 **GitHub 스타 20,000개, 감사합니다!**
🎉 **GitHub 스타 30,000개, 감사합니다!**
REA를 사용하고, 버그를 보고하고, 빌드를 테스트하고, 수정에 기여해 주신 모든 분께 감사드립니다.
REA를 사용하고, 버그를 보고하고, 기능을 요청하고, 빌드를 테스트하고, 수정에 기여해 주신 모든 분께 감사드립니다.
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
+3 -3
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · **Polski** · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · **Polski** · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
# REA: Reverse Engineer Anything
@@ -264,9 +264,9 @@ Zobacz [CONTRIBUTING.md](CONTRIBUTING.md), aby poznać konfigurację środowiska
## Historia gwiazdek
🎉 **20 000 gwiazdek na GitHubie — dziękujemy!**
🎉 **30 000 gwiazdek na GitHubie — dziękujemy!**
Dziękujemy wszystkim, którzy używają REA, zgłaszają błędy, testują kompilacje i przesyłają poprawki.
Dziękujemy wszystkim, którzy używają REA, zgłaszają błędy, proponują nowe funkcje, testują kompilacje i przesyłają poprawki.
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
+3 -3
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · **Português (Brasil)** · [العربية](README_ar.md) · [فارسی](README_fa.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · **Português (Brasil)** · [العربية](README_ar.md) · [فارسی](README_fa.md)
# REA: Faça engenharia reversa de qualquer coisa
@@ -264,9 +264,9 @@ Consulte [CONTRIBUTING.md](CONTRIBUTING.md) para configurar o ambiente de desenv
## Histórico de estrelas
🎉 **20.000 estrelas no GitHub — muito obrigado!**
🎉 **30.000 estrelas no GitHub — muito obrigado!**
Obrigado a todos que usam o REA, relatam bugs, testam builds e contribuem com correções.
Obrigado a todos que usam o REA, relatam bugs, sugerem funcionalidades, testam builds e contribuem com correções.
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
+3 -3
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · **Русский** · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · **Русский** · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
# REA: Реверс-инжиниринг чего угодно
@@ -264,9 +264,9 @@ npx rea-agents@latest setup
## История звёзд
🎉 **20 000 звёзд на GitHub — спасибо!**
🎉 **30 000 звёзд на GitHub — спасибо!**
Спасибо всем, кто использует REA, сообщает об ошибках, тестирует сборки и помогает с исправлениями.
Спасибо всем, кто использует REA, сообщает об ошибках, предлагает новые функции, тестирует сборки и помогает с исправлениями.
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
+3 -3
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · **ไทย** · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · **ไทย** · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
# REA: Reverse Engineer Anything
@@ -264,9 +264,9 @@ npx rea-agents@latest setup
## ประวัติดาว
🎉 **20,000 ดาวบน GitHub — ขอบคุณทุกคน!**
🎉 **30,000 ดาวบน GitHub — ขอบคุณทุกคน!**
ขอบคุณทุกคนที่ใช้ REA รายงานบั๊ก ทดสอบบิลด์ และช่วยแก้ไข
ขอบคุณทุกคนที่ใช้ REA รายงานบั๊ก เสนอฟีเจอร์ ทดสอบบิลด์ และช่วยแก้ไข
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
+3 -3
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · **Türkçe** · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · **Türkçe** · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
# REA: Her şeyi tersine mühendislikle inceleyin
@@ -264,9 +264,9 @@ Geliştirme ortamı ve kontroller için [CONTRIBUTING.md](CONTRIBUTING.md), doğ
## Yıldız geçmişi
🎉 **GitHub'da 20.000 yıldız — teşekkürler!**
🎉 **GitHub'da 30.000 yıldız — teşekkürler!**
REA'yı kullanan, hata bildiren, derlemeleri test eden ve düzeltmelere katkıda bulunan herkese teşekkürler.
REA'yı kullanan, hata bildiren, özellik talep eden, derlemeleri test eden ve düzeltmelere katkıda bulunan herkese teşekkürler.
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
+3 -3
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · **Українська** · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · **Українська** · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
# REA: Реверс-інжиніринг будь-чого
@@ -264,9 +264,9 @@ npx rea-agents@latest setup
## Історія зірок
🎉 **20 000 зірок на GitHub — дякуємо!**
🎉 **30 000 зірок на GitHub — дякуємо!**
Дякуємо всім, хто використовує REA, повідомляє про помилки, тестує збірки й допомагає з виправленнями.
Дякуємо всім, хто використовує REA, повідомляє про помилки, пропонує нові функції, тестує збірки й допомагає з виправленнями.
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
+3 -3
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · **Tiếng Việt** · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · **Tiếng Việt** · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
# REA: Kỹ thuật dịch ngược cho mọi thứ
@@ -264,9 +264,9 @@ Xem [CONTRIBUTING.md](CONTRIBUTING.md) về thiết lập môi trường phát t
## Lịch sử sao
🎉 **20.000 sao trên GitHub — xin cảm ơn!**
🎉 **30.000 sao trên GitHub — xin cảm ơn!**
Cảm ơn mọi người đã sử dụng REA, báo lỗi, kiểm thử bản dựng và đóng góp bản sửa lỗi.
Cảm ơn mọi người đã sử dụng REA, báo lỗi, đề xuất tính năng, kiểm thử bản dựng và đóng góp bản sửa lỗi.
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
+3 -3
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · [简体中文](README_zh.md) · **繁體中文** · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
[English](README.md) · [简体中文](README_zh.md) · **繁體中文** · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
# REA:逆向分析一切
@@ -266,9 +266,9 @@ npx rea-agents@latest setup
## Star 歷史
🎉 **GitHub Star 達到 20,000 個,感謝大家!**
🎉 **GitHub Star 達到 30,000 個,感謝大家!**
感謝每一位使用 REA、報告錯誤、測試建置和貢獻修復的朋友。
感謝每一位使用 REA、報告錯誤、提出功能需求、測試建置和貢獻修復的朋友。
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
+3 -3
View File
@@ -1,6 +1,6 @@
<div align="center">
[English](README.md) · **简体中文** · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
[English](README.md) · **简体中文** · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
# REA:逆向分析一切
@@ -266,9 +266,9 @@ npx rea-agents@latest setup
## Star 历史
🎉 **GitHub Star 达到 20,000 个,感谢大家!**
🎉 **GitHub Star 达到 30,000 个,感谢大家!**
感谢每一位使用 REA、报告错误、测试构建和贡献修复的朋友。
感谢每一位使用 REA、报告错误、提出功能需求、测试构建和贡献修复的朋友。
<a href="https://www.star-history.com/?repos=morluto%2Frea&amp;type=date">
<picture>
+32 -4
View File
@@ -866,6 +866,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
JsonArray operands = new JsonArray();
JsonArray references = new JsonArray();
JsonArray destinations = new JsonArray();
JsonArray flowEvidence = new JsonArray();
JsonObject flow = new JsonObject();
flow.addProperty("kind", "unavailable");
flow.addProperty("conditional", false);
@@ -881,7 +882,20 @@ public final class ReaGhidraBridge extends HeadlessScript {
result.addProperty("mnemonic", instruction.getMnemonicString());
result.addProperty("raw_disassembly", instruction.toString());
RefType type = instruction.getFlowType();
flow.addProperty("kind", type.isCall() ? "call" : type.isJump() ? "jump" : type.isTerminal() ? "terminal" : "fallthrough");
JsonObject listingFlow = new JsonObject();
listingFlow.addProperty("source", "ghidra-listing-flow-type");
listingFlow.addProperty("value", type.getName());
flowEvidence.add(listingFlow);
boolean decodedReturn = false;
for (PcodeOp operation : instruction.getPcode()) {
if (operation.getOpcode() != PcodeOp.RETURN) continue;
decodedReturn = true;
JsonObject returnEvidence = new JsonObject();
returnEvidence.addProperty("source", "ghidra-instruction-pcode");
returnEvidence.addProperty("value", operation.getMnemonic());
flowEvidence.add(returnEvidence);
}
flow.addProperty("kind", type.isCall() ? "call" : type.isJump() ? "jump" : decodedReturn ? "return" : type.isTerminal() ? "terminal" : "fallthrough");
flow.addProperty("conditional", type.isConditional());
flow.addProperty("computed", type.isComputed());
if (!type.isComputed()) for (Address target : instruction.getFlows()) destinations.add(canonicalAddress(target));
@@ -917,10 +931,13 @@ public final class ReaGhidraBridge extends HeadlessScript {
}
}
flow.add("direct_destinations", destinations);
flow.add("classification_evidence", flowEvidence);
result.add("operands", operands);
result.add("flow", flow);
result.add("references", references);
result.add("limitations", GSON.toJsonTree(List.of("Operand components are Ghidra Listing decoder tokens; effective memory base/index/displacement roles and per-instruction context mode are unavailable. The mode field is the program language variant.", "Terminal flow is not classified as a return without provider evidence.")));
List<String> limitations = new ArrayList<>(List.of("Operand components are Ghidra Listing decoder tokens; effective memory base/index/displacement roles and per-instruction context mode are unavailable. The mode field is the program language variant."));
if (flow.get("kind").getAsString().equals("terminal")) limitations.add("No RETURN operation was observed in the provider's instruction p-code; terminal classification remains generic.");
result.add("limitations", GSON.toJsonTree(limitations));
return result;
}
@@ -1629,9 +1646,20 @@ public final class ReaGhidraBridge extends HeadlessScript {
Function function = entry.function;
Symbol symbol = function.getSymbol();
String qualified = symbol == null ? function.getName() : symbol.getName(true);
if (function.getName().equals(value) || qualified.equals(value)) {
matches.add(function);
boolean matchesName = function.getName().equals(value) || qualified.equals(value);
if (!matchesName) {
// Imported labels (for example Mach-O _main) can share a
// function entry without becoming its primary symbol. Only
// exact entries qualify; an interior label is not a selector.
for (Symbol entrySymbol : currentProgram.getSymbolTable().getSymbols(function.getEntryPoint())) {
monitor.checkCancelled();
if (entrySymbol.getName().equals(value) || entrySymbol.getName(true).equals(value)) {
matchesName = true;
break;
}
}
}
if (matchesName) matches.add(function);
}
if (matches.size() > 1)
throw new RequestFailure("ambiguous", "Ghidra procedure name is ambiguous: " + value +
+6 -1
View File
@@ -553,7 +553,12 @@ def _search_results(document, kind, params):
if not matches(item[1]):
continue
selected.append(item)
return [{"address": address, **_string_inventory(document)[address]} for address, _ in selected] if kind == "string" else [
if not selected:
return []
if kind == "string":
records = _string_inventory(document)
return [{"address": address, **records[address]} for address, _ in selected]
return [
{
"address": address,
"value": value,
@@ -252,7 +252,7 @@ No request can cross a concurrent target or provider transition.
canonical path, source digest, generic kind and format, selected architecture,
and available architectures. Hopper loader arguments leave the domain model.
Provider-owned databases use a generic analysis-database classification; their
adapter determines support. A legacy `.hop` target is therefore accepted only
adapter determines support. A `.hop` analysis database is therefore accepted only
by Hopper without teaching the domain how to open it. Ghidra v1 continues to use
private temporary projects and does not open user projects.
@@ -336,19 +336,19 @@ 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 while preserving legacy records
### 8. Commit concrete or explicitly unknown analysis profiles in Evidence
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.
Every Evidence record includes `analysis_profile`, either the canonical profile
envelope above or `null` when no profile was observed. Deep-analysis observations
commit the resolved provider ID, version, and effective configuration in this
profile. The field participates in the Evidence semantic ID.
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
replay, or support an "unchanged" conclusion that depends on identical analysis
semantics.
An explicitly unknown profile can be displayed and cited, but cannot establish
profile compatibility, seed snapshot replay, or support an "unchanged"
conclusion that depends on identical analysis semantics. Snapshot query entries
must have matching profile-bound Evidence; imports reject missing or mismatched
observations instead of silently dropping query entries. Both `entries` and
`workflow_entries` are required arrays, including when empty.
Different providers or profile digests are never snapshot-compatible. A
derived comparison may compare cross-provider normalized facts only when its
@@ -407,12 +407,12 @@ means no dynamic provider has supplied a snapshot. Once one or more start,
snapshots are not live inventories, and a point-in-time observation never proves
historical absence.
The existing flat `provider`, `providers`, and `capabilities` status fields stay
during the 1.x compatibility window. Their effective deep-operation entries
come only from the active binding; target-free ambiguity is unavailable rather
than arbitrarily collapsed. The new candidate fields are authoritative for
selection. A future major release may remove the synthetic composite provider
field after callers have migrated.
Status exposes `analysis_provider_candidates` for configured deep providers and
`analysis_provider_binding` for the selected concrete provider and profile.
`capabilities` retains effective operation availability and exact provider
provenance, including auxiliary operation families. There are no synthetic
flat `provider` or `providers` aliases. Provider prompt completion uses the
actual selectable candidates.
Selection failures extend the tagged `ProviderSelectionError` with a stable
reason:
@@ -448,26 +448,20 @@ snapshot's provider.
- Tool names and normalized output contracts remain provider-neutral. REA does
not add `ghidra_decompile` or `hopper_decompile` tools.
## Compatibility and migration
## Provider and snapshot representations
| Existing behavior or data | Decision |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| No provider selector, only Hopper usable | `auto` selects the sole candidate; behavior remains equivalent |
| No deep provider usable | Automatic open may remain unbound for auxiliary operations; deep capabilities explain why they are unavailable |
| Hopper and Ghidra both usable, no preference | Open fails with `ambiguous`; installation order never becomes policy |
| Existing Hopper environment settings | Remain supported as Hopper adapter configuration |
| `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 | 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 |
The deprecated `loaderArgs` response must never be read to select a provider or
validate a snapshot. It is empty for non-Hopper bindings and may be removed only
with a separately documented major contract change.
| Behavior or data | Contract |
| -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| No selector, one usable deep provider | `auto` selects the sole candidate |
| No usable deep provider | Automatic open may remain unbound for auxiliary operations; deep capabilities explain their unavailability |
| Several usable deep providers without a preference | Open fails with `ambiguous`; installation order never becomes policy |
| Hopper environment settings | Adapter configuration committed into the typed analysis profile, including effective loader arguments |
| `open_binary` output | Provider-neutral target identity only; no `loaderArgs` projection |
| `.hop` input | Analysis database accepted only by Hopper |
| Snapshots | Concrete provider/profile binding, required primitive and workflow entry arrays, and matching authenticated Evidence |
| Evidence without an observed profile | Explicit `analysis_profile: null`; ineligible for profile-dependent replay |
| Snapshot save receipt | `primitive_entries`, `workflow_entries`, and `evidence_records` counts without a duplicate `entries` alias |
| Provider failure after selection | Returned from that provider, without transparent retry through another engine |
## Required behavior scenarios
+40 -1
View File
@@ -23,10 +23,37 @@ export REA_JADX_MCP_JAR=/absolute/path/jadx-headless-mcp-0.7.1-all.jar
export JAVA_HOME=/absolute/path/existing-jdk
```
On Windows x64, use a REA package with its matching bundled Windows native
controls **including owned protocol stdin support**. This Windows boundary is
new on repository main; an older released package can still reject this host.
PowerShell accepts paths containing spaces without additional escaping
inside these environment values:
```powershell
$env:REA_JADX_MCP_JAR = 'D:\tools\jadx-headless-mcp-0.7.1-all.jar'
$env:JAVA_HOME = 'D:\tools\jdk-21'
rea inspect-android-package 'D:\targets\Example.apk'
```
REA selects `JAVA_HOME\bin\java.exe`, or resolves `java.exe` on the selected
Windows `PATH`. A missing or mismatched native artifact reports the package
prerequisite rather than attempting an unowned Java launch. Windows arm64 is
outside this provider's current native boundary.
Readiness checks the selected JAR's ZIP directory for the classes consumed by
REA's metadata bridge, as well as Java's compiler and version modules. It does
not load an APK or execute engine code. Invalid archives and missing bridge
classes report JAR-specific recovery; classpath inventory alone does not prove
that every class or engine operation will load successfully. Discovery limits
each ZIP metadata read to 8 MiB before allocation; a larger directory reports
the exhausted read budget and the selected JAR path.
The current metadata bridge is verified on macOS arm64 with OpenJDK 21 and the
public Appium ApiDemos fixture. The POSIX adapter also supports Linux; the new
bridge has not yet undergone real verification on Linux/JDK 25.
Windows is unsupported for this provider's owned stdio process boundary.
Windows x64 uses an owned Job Object with bidirectional stdio, atomic job
assignment and kill-on-owner-close. Real Windows verification uses JDK 21 and
the pinned JADX 0.7.1 JAR; see the real verification lane below.
## Answer an analyst question
@@ -130,6 +157,12 @@ target/configuration changes, failure, timeout, cancellation, MCP disconnect or
server shutdown. One-shot CLI commands always join cleanup before returning.
REA never writes to the original APK or launches target code.
On Windows, stdin writes preserve stream backpressure and run outside the Node
event loop. Cancellation terminates the retained job, including descendants,
before workspace cleanup. An abrupt CLI/server process termination closes the
job and stops Java, but cannot run JavaScript workspace cleanup; temporary
workspace files can remain after that forced exit.
If Java cannot start the bridge, REA reports the selected executable and retained
startup diagnostics. Select a full JDK through `JAVA_HOME` or `PATH`, and use
`java --list-modules` to confirm `jdk.compiler` is present. A runnable JRE alone
@@ -189,6 +222,12 @@ without the compiler module. It checks real Java startup failures, missing
selectors, recovery after failures, and CLI cancellation with owned cleanup.
The original APK and host Java configuration remain unchanged.
The same lane runs on Windows x64 with the bundled native controls and a full
JDK. It checks CLI/MCP results, real MCP cancellation and disconnect, and Java
exit after abrupt CLI owner termination. Windows forced termination is checked
separately from the POSIX CLI SIGTERM handler; it does not claim a structured
cancellation result or workspace removal after the owner is forcibly killed.
The download script verifies fixed SHA-256 values, reuses matching files and
refuses to overwrite a different existing file. APK/JAR files live under ignored
`_reference/` and are excluded from package contents. The lane exercises actual
+8
View File
@@ -31,6 +31,14 @@ overall coverage becomes `partial` and a limitation explains the omission.
Duplicate inventory pages are merged by artifact identity and occurrence path;
their Evidence IDs remain attached as source Evidence.
Content nodes identify bytes by SHA-256 and report only content-derived formats.
Each occurrence reports its own `artifact_kind`, `artifact_format`, and executable
permission. Identical bytes can therefore appear as a script, resource, or native
addon at different paths without overwriting another occurrence's role. Package
formats such as APK and IPA belong to the occurrence; their ZIP bytes share a
content format. Graph and directory identities use locale-independent Unicode
ordering.
For IPA inventories, archive components outside every application root remain
in the component lists. Their JavaScript and native candidates are grouped with
other unrooted components, preserving the inventory projection's path pairing
+7
View File
@@ -84,6 +84,13 @@ uses isolated mode and an owned cache. Exact upstream profiles are recorded in
stack can accompany an unknown (`null`) NX indicator. ET_DYN can be a shared
library, and an absent canary symbol does not prove every function unprotected.
When complete layout Evidence is already retained, `inspect_analysis_view` /
`inspect-analysis-view` projects a summary, the mitigations or linkage facet,
one section or symbol, or a stable page without re-running the decoder. CLI
one-shot invocations pass portable inline Evidence JSON; an MCP session uses
the retained `evidence_id`. Page `limit` is required and bounded by the
measured MCP stdio budget documented on the tool contract.
Complete results have a 32 MiB input, 64 MiB reply, 1 MiB combined diagnostics
and 30-second owned command deadline. The reply budget applies to the decoder
record; the CLI/MCP Evidence envelope adds copies and encoding overhead. The
+7
View File
@@ -109,6 +109,13 @@ cached result. `close_binary` accepts `snapshot_path` and optional
`overwrite: true` to save before releasing provider resources. A failed save
leaves the session open so the caller can resolve the output failure.
The MCP save receipt reports `primitive_entries`, `workflow_entries`, and
`evidence_records` separately. Zero primitive bindings can still accompany retained workflow
results and Evidence. These are cached observations, not a saved provider
database: only eligible exact queries can reuse a result, and new or live
queries can still require provider startup. The CLI uses the same snapshot
format and preserves these records when loading and updating it.
## Import, export and compare Evidence
Evidence records retain artifact identity, source locations, observations,
+85 -8
View File
@@ -195,8 +195,9 @@ Before applying changes, REA checks your current configuration. The plan lists:
Malformed or unsafe existing configuration blocks the whole transaction before
Hopper installation or any file write. Declining or pressing Ctrl-C makes no
changes. Agent configuration writes preserve unrelated
entries, create backups, use atomic replacement, and verify their result.
changes. Agent configuration writes preserve unrelated entries and comments,
create backups, use atomic replacement, and verify their result. Setup and
uninstall retain an existing `.rea.backup` rather than replacing the first snapshot.
After setup, REA reports which agents, analysis tools, and workflow files passed
its final checks. Restart any agent named in the completion message, then begin
@@ -310,6 +311,19 @@ host mount and the rest of `/tmp` remain unchanged; this fallback never invokes
`sudo`. `rea doctor --provider hopper --json` reports the selected
strategy and both host and effective mount facts.
The supported Linux demo forwards additional launches into its existing
application, even across private displays. REA therefore reserves one Linux
Hopper application per user across cooperating REA processes. A competing
CLI or MCP session receives the owning session ID before Hopper is launched,
for both the same target and a different target. Switch targets through that
MCP session, or close it before starting another session. Closing the owner
releases the reservation. This does not attach to or claim ownership of
manually opened Hopper applications.
Stale lease recovery is serialized. If an interrupted recovery leaves its
reservation directory, the diagnostic identifies that exact path. Stop REA
sessions before removing that recovery directory and retrying.
### Launcher paths and troubleshooting
On macOS, REA uses
@@ -479,14 +493,14 @@ installs, upgrades, or modifies Ghidra or Java.
Each verified session uses an ephemeral temporary project and isolated
home/cache/config/temp paths. REA passes `-readOnly`, `-deleteProject`, uses
Ghidra's default analysis and resource settings, and loads its packaged Java
Ghidra's default analysis settings, and loads its packaged Java
bridge via `-scriptPath`; it never opens an existing user project. Linux and
macOS use a current-user-only local bridge socket and descriptor. The
project remains under the selected temporary directory. If its Unix socket
pathname would exceed the host's byte limit, REA allocates a separate mode-0700
socket directory under `/tmp` and removes it on close, cancellation, or failure.
Diagnostics retain the actual endpoint and both owned directories.
On macOS, REA starts the inspected JVM directly using Ghidra's own LaunchSupport
On Linux and macOS, REA starts the inspected JVM directly using Ghidra's own LaunchSupport
configuration. Apple platform shell wrappers hide their environments from
ownership inspection, so retaining those wrappers would prevent verified
process-group cancellation during startup.
@@ -501,8 +515,9 @@ import and auto-analysis. The first Ghidra-backed query starts that work lazily.
The provider startup deadline is 330,000 ms by default for import, analysis,
bridge, and health readiness. Large binaries can need more: set
`REA_GHIDRA_STARTUP_TIMEOUT_MS` to an integer between 1 and 2,147,483,647
milliseconds in the server environment. An absent, empty, invalid or out-of-range
value keeps the default. REA parses the supplied configuration environment and
milliseconds in the server environment. An absent setting uses the default;
empty, invalid or out-of-range supplied values fail configuration validation.
REA parses the supplied configuration environment and
passes the deadline to each provider client; a startup failure is returned by the query that triggered it,
without exposing partial analysis. This deadline is separate from MCP transport
initialization and the client's deadline for that individual tool call. A client
@@ -519,6 +534,65 @@ 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.
### Ghidra heap and CPU controls
Set resource controls in the environment that launches the CLI or MCP server,
before opening a new Ghidra session. On Linux and macOS, REA chooses the first
nonempty setting in this order: `GHIDRA_HEADLESS_MAXMEM`, `GHIDRA_MAXMEM`, then
`2G`. The chosen value becomes the JVM's `-Xmx` argument. Use a JVM heap-size
value such as `512M` or `2G`; an invalid value causes Java startup to fail.
The supported Ghidra 12.1.4 Windows headless script consumes the same settings
with the same precedence.
For a small target on a constrained host:
```bash
export GHIDRA_HEADLESS_MAXMEM=512M
```
In PowerShell:
```powershell
$env:GHIDRA_HEADLESS_MAXMEM = "512M"
```
An MCP client must pass this setting to its REA server process; changing an
unrelated terminal's environment does not reconfigure an existing server or
JVM. A 512 MiB heap was verified with small native fixtures, but larger targets
can require more memory. The Java heap limit does not bound JVM RSS, Node's
heap, or total memory used by the process family. The startup deadline controls
waiting time independently of these memory settings.
REA clears `_JAVA_OPTIONS`, `JAVA_TOOL_OPTIONS`, `JDK_JAVA_OPTIONS`, and
`GHIDRA_JAVA_OPTIONS` inherited from the caller, and owns
`GHIDRA_HEADLESS_JAVA_OPTIONS`. Custom flags injected through these variables
are not supported configuration. On Windows, REA replaces `JDK_JAVA_OPTIONS`
with its own isolated home/temp settings and `-XX:-UsePerfData`; on Linux and
macOS it supplies isolated paths directly to Java. Use the supported heap
settings above instead of adding a second `-Xmx` through an injection variable.
The Linux/macOS launch sets `-XX:ParallelGCThreads=2` and
`-XX:CICompilerCount=2`; the supported Windows headless script also sets these
counts. These constrain particular JVM thread pools, not all analysis threads
or CPU usage. REA does not pass Ghidra's `-max-cpu` option or expose a general
Ghidra CPU limit. When needed, apply host CPU affinity and scheduling controls
to the REA process and verify the resulting JVM's affinity. Select CPUs allowed
on the host; do not assume CPU numbers from another machine are valid.
Verify controls at their consumer after the first Ghidra-backed query starts
the JVM: inspect that owned JVM's effective `-Xmx` and GC/compiler flags, CPU
affinity, and observed memory use. Do not infer them from the parent shell's
environment or the absence of an error. On Linux, `/proc/<jvm-pid>/cmdline`
contains the launch arguments and `/proc/<jvm-pid>/status` reports
`Cpus_allowed_list` and `VmRSS`. Inspect only the process belonging to this
session and retain the resource flags needed for the measurement rather than
copying full command lines or environments into reports. Heap and affinity
settings affect cost and latency; they are not part of REA's semantic analysis
profile identity. See [real-provider testing](testing.md) for bounded
verification workflows.
### Ghidra diagnostics and verification
Unexpected request failures retain their original internal cause. CLI and MCP
errors expose Error names, messages, and available codes under
`details.diagnostics.failure_cause`; other rejection values retain their
@@ -560,8 +634,11 @@ rea uninstall --purge-data
Uninstall preserves Hopper, Node.js, Evidence files, captures, unrelated skills
and other MCP servers. Purging removes only REA's cache and state under
`~/.rea`; malformed client configuration or purge-path symlinks stop the
operation. See the [CLI guide](cli.md#output-and-exit-status) for exit statuses.
`~/.rea`. A client configuration that is malformed, unreadable, or at an unsafe
path stops the operation before anything is removed, and a client that fails
while being updated stops the remaining removals. A purge path that is a
symbolic link is retained and reported rather than followed. See the
[CLI guide](cli.md#output-and-exit-status) for exit statuses.
## MCP Registry
+37 -15
View File
@@ -2,12 +2,15 @@
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.
are `inspect_analysis_view`, `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. `inspect_analysis_view` projects a
summary, module page, or one module from already completed application
Evidence; it does not walk relationships.
These workflows consume Evidence produced by
`inspect_analysis_view` also projects retained `inspect_binary_layout`
Evidence. The remaining 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
@@ -138,14 +141,33 @@ Nested callable returns are not assigned to their parent callable. Projected
graph observations carry source ranges but never source text.
Return variants pair only when a literal discriminant such as `/type` has one
unique occurrence on each side and pairing is reciprocal. Changes use JSON
Pointer paths with `added`, `removed`, `changed`, or `unknown` status. A missing
field is added or removed only when the relevant parent-property coverage is
complete on both shapes. The output includes exact selector candidates,
omissions, Evidence links, coverage, and limitations; it does not execute
JavaScript. When runtime semantics matter, run behavioral probes directly
against the relevant application versions and capture them through the
available browser, Electron, or process workflows.
unique occurrence on each side and pairing is reciprocal. Source order never
implies correspondence. Each retained variant contributes a property inventory
of observed JSON Pointer names, including fields whose static values stay
unknown, plus that variant's parent-property coverage.
Changes use JSON Pointer paths with `added`, `removed`, `changed`, or `unknown`
status and a separate `presence` object (`present`, `absent`, or
`unknown-coverage`) on each side. A name is `added` or `removed` when one paired
shape proves presence and the other proves absence. Complete parent-property
coverage can establish absence even when field values stay unresolved. Identical literals are
omitted. Unresolved values that remain on both sides stay `unknown` when the
projections differ, and are omitted when presence is unchanged and the unknown
projections match. Incomplete spreads and other partial parent coverage keep
one-sided names `unknown` with `unknown-coverage` on the incomplete side.
Unpaired variants remain visible as unknown changes and still list their
property inventories, each with its own `source_range`. Inventories exclude
array holes and slots whose presence was invalidated by mutation; an unresolved
value alone does not make an observed property uncertain. `summary.added` and
`summary.removed` count
presence-level add/remove as well as literal value add/remove;
`summary.unknown` does not absorb complete-coverage presence-only gaps.
The output includes exact selector candidates, omissions, Evidence links,
coverage, and limitations; it does not execute JavaScript. When runtime
semantics matter, run behavioral probes directly against the relevant
application versions and capture them through the available browser, Electron,
or process workflows.
## CLI and verification
@@ -167,8 +189,8 @@ writeFileSync("trace-input.json", JSON.stringify({
rea trace-application-feature ./trace-input.json --json
```
MCP analysis returns an envelope containing `result`, `evidence_id`, and full
`evidence`. If the connected server advertises retained references, reuse its
MCP analysis returns the complete Evidence record, with the operation result
in `normalized_result` and its identity in `evidence_id`. If the connected server advertises retained references, reuse its
exact returned ID as `{"kind":"retained-evidence","evidence_id":"RETURNED_ID"}`
in `application`, or `left`/`right` for comparisons. `RETURNED_ID` is a template,
not a literal valid ID. Native Evidence arrays still use complete records.
+7 -6
View File
@@ -108,14 +108,15 @@ memory needed to construct the analysis graph itself.
MCP prepares the complete repeated response incrementally against the pinned
SDK's 10 MiB stdio receive-buffer budget. Oversized results return an actionable
transport constraint and the exact same-session Evidence reference. Use
`trace_application_feature` to inspect a selected module's relationships, or
`export_evidence_bundle` to write the complete canonical bundle without a
document-sized allocation. Same-session analysis reads reuse authenticated
immutable snapshots; foreign inline Evidence is still parsed and authenticated.
`inspect_analysis_view` with the retained Evidence ID for a summary, one
module, or a stable page of module identities. Use `trace_application_feature`
once a module, route, or string seed is known, or `export_evidence_bundle` to
write the complete canonical bundle without a document-sized allocation.
Same-session analysis reads reuse authenticated immutable snapshots; foreign
inline Evidence is still parsed and authenticated.
See [MCP tool results](mcp-contracts.md#tool-results) for larger client buffers
and `REA_MCP_MAX_RESPONSE_BYTES`. Follow-up results remain complete and can also
exceed the transport budget. Compact result views are tracked separately in
[#1050](https://github.com/morluto/rea/issues/1050).
exceed the transport budget.
## What is reconstructed
+58 -32
View File
@@ -2,7 +2,7 @@
## Generated catalog
Run `npm run build:cached` in a source checkout to generate the machine-readable
Run `npm run docs:generate` in a source checkout to generate the machine-readable
catalog at `docs/public/product-catalog.json`. Documentation deployments serve
the same file at [/rea/product-catalog.json](/rea/product-catalog.json). PR CI retains it with the packaged
skill and portable conformance projections in the `generated-docs` artifact.
@@ -24,22 +24,28 @@ health transition leaves that catalog unchanged and does not emit
`notifications/tools/list_changed`.
Advertised input and output schemas contain no reachable recursive references.
The input compatibility profile limits nesting to ten object, array, and
`anyOf`/`oneOf`/`allOf` levels, following local references per path. Property
maps, reference definitions, and example data do not add schema levels. Tests
check the complete catalog after SDK conversion and its generated counterpart.
This is REA's local compatibility profile; individual model APIs can impose
additional limits.
Schemas share repeated definitions through schema-local references while
preserving complete fields and validation rules. Input properties retain their
descriptions and literal examples. The SDK advertises an object root for
canonical object unions and retains each branch's requirements and exclusions.
Tests validate the schemas after SDK conversion and compare advertised
validation with actual calls. Individual model APIs can impose additional
nesting limits; complete producer captures can exceed ten structural levels.
For passive `compare_web_captures` inputs, pass each complete
`inspect_web_page` result in `before.inspection` or `after.inspection`, with an
optional complete `discover_webmcp_tools` result in the matching `webmcp` field.
The input schema describes these producer-result objects as round-trip payloads.
For scenario comparisons, pass complete `capture_browser_scenario` results in
`before_scenario` and `after_scenario`. REA validates all nested fields with the
original capture schemas; observation output schemas remain complete. Scenario
authoring inputs and comparison normalization options retain their full
advertised structure.
`compare_web_captures` accepts exactly one of two input shapes:
- Passive: `before` and `after` each contain `inspection`, the complete
`normalized_result` from `inspect_web_page`. Each may also contain `webmcp`,
the complete `normalized_result` from `discover_webmcp_tools`, or null.
- Scenario: `before_scenario` and `after_scenario` contain complete
`normalized_result` objects from `capture_browser_scenario`. Optional
`normalization` defaults to `{ "rules": [] }`.
The advertised schema includes complete nested capture fields and rejects
incomplete pairs, mixed comparison families, and structurally malformed
captures. Domain validation additionally checks relationships such as event
sequence references and retained counts; JSON Schema does not express those
cross-field invariants.
Call `binary_session` with `{}` and read `result.tool_availability` to choose a
callable operation for the current target, provider, host, and negotiated client
@@ -97,9 +103,11 @@ still run synchronously, so control messages can wait for those
phases to release the event loop. A rejected client promise alone does not
establish that the server has stopped its work.
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.
Direct CLI analysis calls work without a progress token and translate SIGINT or
SIGTERM into the same AbortSignal used by providers. Signal guards remain active
through provider cleanup, including repeated delivery by package runners.
Existing controlled-process cleanup and provider shutdown rules still apply;
REA never kills a process it cannot prove it owns.
## Ghidra first-query deadlines and recovery
@@ -111,11 +119,11 @@ auto-analysis, bridge connection, and health readiness before returning analysis
These deadlines have different owners:
| Deadline | Owner and effect |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| MCP initialize | The client bounds transport/REA connection startup, before any target query. |
| Ghidra startup | REA allows 330,000 ms by default; `REA_GHIDRA_STARTUP_TIMEOUT_MS` accepts 1–2,147,483,647 ms. Invalid values keep the default. Startup failure is reported by the first provider query. |
| Individual tool call | The client bounds its wait, including cold engine startup. The pinned client SDK 2.3.1 defaults to 60,000 ms and can cancel earlier than REA's startup deadline. |
| Deadline | Owner and effect |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| MCP initialize | The client bounds transport/REA connection startup, before any target query. |
| Ghidra startup | REA allows 330,000 ms by default; `REA_GHIDRA_STARTUP_TIMEOUT_MS` accepts decimal integers from 1–2,147,483,647 ms. Invalid supplied values fail configuration validation. Startup failure is reported by the first provider query. |
| Individual tool call | The client bounds its wait, including cold engine startup. The pinned client SDK 2.3.1 defaults to 60,000 ms and can cancel earlier than REA's startup deadline. |
For an already connected client using the pinned SDK, request options are the
**second** argument of `callTool`:
@@ -171,26 +179,32 @@ increasing a client deadline alone does not fix those failures.
## Tool results
Evidence-producing tools return `{ result, evidence_id, evidence }` in both
text and structured content. `evidence` is the complete canonical Evidence
record, including `normalized_result`, which equals `result`. The same record
is retained in the session bundle. Read `result` directly, or pass `evidence`
to a compatible comparison tool: `analyze_function` Evidence can be passed
Evidence-producing tools return the complete canonical Evidence record in both
text and structured content. Read `structuredContent.normalized_result` for the
operation result and `structuredContent.evidence_id` for its identity. The same
record is retained in the session bundle. `analysis_profile` is always present:
a concrete profile object or `null`; either value participates in semantic
identity. Records omitting this field are rejected. Pass the returned Evidence
directly to a compatible comparison tool: `analyze_function` Evidence can be passed
directly to `compare_functions`, and `inspect_artifact` Evidence to
`compare_artifacts`. Use `get_evidence_bundle` when the task needs broader
retained session history or an explicit bundle for transfer.
REA prepares complete MCP results within the pinned stdio client's 10 MiB
receive-buffer budget, including both text and structured representations and
room for the JSON-RPC envelope. If a result cannot fit, REA returns
room for the JSON-RPC envelope. Response budget settings are captured at startup;
restart or recreate the server to apply changes. If a result cannot fit, REA returns
`resource_constraint` with `details.resource: "transport"` before constructing
a document-sized string. Analysis Evidence remains complete in the current
session. Its exact reference is reported in
`details.reported_limits.evidence_reference`; use it with a focused application
workflow, or call `export_evidence_bundle` with a destination path. Complete
`details.reported_limits.evidence_reference`; call `inspect_analysis_view` with
that reference for a summary, one section/module, or a stable page, or call
`export_evidence_bundle` with a destination path. Complete
bundle exports stream canonical JSON into an atomically published file. A broad
follow-up or `get_evidence_bundle` can also exceed the response budget; exporting
preserves the complete session without sending it through a single MCP frame.
Cancelling an export stops further serialization and removes its staging file.
The destination changes only when a complete export is atomically published.
Clients that explicitly configure a larger receive buffer can set the REA
server's `REA_MCP_MAX_RESPONSE_BYTES` environment variable to the same byte
@@ -202,6 +216,18 @@ Ordinary responses keep their existing complete result contract.
## Retained application Evidence inputs
`inspect_analysis_view` projects a caller-selected view of already completed
`inspect_binary_layout` or `analyze_javascript_application` Evidence. Source is
an exact same-session retained reference or portable inline Evidence. Views are
a summary, a layout mitigations or linkage facet, one section/symbol/module, or
a stable page with a caller-selected positive `limit`. Module pages include
JavaScript assets, bundled modules, and source modules. Select an exact
`node_id` when a path is ambiguous or unavailable. Module items retain their
recorded property values and source locations; summaries include parent
application and semantic coverage. The result carries a digest of the selected
facts. Actual serialized size determines MCP transport admission: reduce the
page size or export retained Evidence if the view is too large.
`trace_application_feature`, `trace_javascript_semantics`,
`compare_application_versions`, `compare_source_to_bundle`, and
`compare_javascript_export_shapes` accept complete inline application Evidence
+3 -1
View File
@@ -27,7 +27,9 @@ prompt argument or tool explicitly accepts one.
For a JavaScript/Electron application directory, `investigate_feature` points
directly to `analyze_javascript_application` with `input_path`; it does not
require `open_binary`. The CLI equivalent is `rea analyze <directory>`.
require `open_binary`. When that Evidence, or `inspect_binary_layout` Evidence,
is already retained, it suggests `inspect_analysis_view` before tracing a
feature seed. The CLI equivalent is `rea analyze <directory>`.
`open_binary` and the doctor target check admit files and macOS app bundles.
A directory outside that opening route is reported with an available analysis
action; this does not establish support for every directory format.
+28
View File
@@ -128,6 +128,9 @@ an explicit `0x` or address-space prefix selects an address; otherwise an exact
database symbol name takes precedence over a bare hexadecimal address. Thus a
function renamed to `dead` remains selectable by name. Use the names returned
by the inventory, including their namespaces and any platform symbol prefix.
Ghidra also accepts secondary symbols at a function entry, such as an imported
`_main` label. A label inside a function is not a procedure-name selector; use
its address when selecting the containing function.
Overloads can share a fully qualified name; ambiguity errors return every
matching entry address so the caller can select the intended function directly.
@@ -182,10 +185,35 @@ outside-memory and undecodable addresses have separate outcomes. Effective
memory base/index/displacement roles and per-instruction context mode remain
unavailable; `mode` is the program language variant.
Ghidra distinguishes a return using decoded instruction p-code's `RETURN`
operation. `flow.classification_evidence` preserves the Listing flow type and
the observed return evidence; mnemonic spelling alone is not the classifier.
Other terminal or trap flows retain the provider's classification, and this
inspection does not change function extents or prove a calling convention.
The analysis profile commits this decoder behavior. Snapshots from the earlier
profile produce a profile mismatch rather than replaying the former generic
terminal result. The old snapshot's `evidence_bundle` remains valid retained
Evidence: copy that object to a separate JSON file and use `rea evidence-import`
to validate it. Run analysis without the old snapshot and save a fresh one
under the current profile. Other providers and older captures may omit the
classification evidence field.
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.
`batch_decompile` keeps each caller-selected symbol or address in `items[].address`.
The separate `procedure` field reports the canonical entry and the provider's
observed entry label. An interior address therefore remains visible without
being mistaken for the function entry. Identity failures are reported as
`procedure.status: "unknown"`; label failures leave a null name and `name_error`.
Neither failure discards usable pseudocode, and names are not parsed from it.
Resolved entries also appear in the composed Evidence locations. New batch
Evidence commits the revised workflow profile; retained historical Evidence
remains valid. This tool
has no dedicated CLI command; the existing `decompile` command remains the
single-procedure CLI operation.
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
+25
View File
@@ -94,6 +94,31 @@ deleted, modified, or unchanged. These are bounded snapshots, not a syscall
trace; short-lived changes between snapshots may be missed. With no selected
paths, filesystem effects remain unknown.
A missing entry proves creation or deletion only when the corresponding
observation root's path enumeration was exhausted. Otherwise its effect has
`status: "unknown"`, the available before/after state, and a reason; a null
state in this variant means unobserved. Paths below symlinks that REA did not
follow also remain unknown. Partial enumeration of one root does not erase
known effects in another, and unavailable content hashes do not by themselves
make path absence unknown.
Terminal frame `data` is the comparison text after the selected normalization.
New captures also preserve `raw_data` when normalization changes a PTY chunk;
otherwise `data` is already the original text. Older captures may lack the
original text for changed chunks. Both values participate in Evidence identity,
while comparisons and trace assertions use the normalized text. Retaining the
original adds at most `limits.output_bytes` of text, because it comes from the
same admitted PTY chunks rather than a second unbounded stream.
Port normalization recognizes explicit `port`, `tcp_port`, `udp_port`, and
`listen` fields, URL authorities, IP endpoints, and `localhost` endpoints, with
ports from 0 through 65535. It preserves ordinary counters, dimensions, file
line numbers, and ambiguous bare host labels. Use explicit literal `patterns`
for a different application-specific spelling. Set `ports: false` to preserve
all endpoint numbers in comparison text. Captures commit the port-normalization
version so older broad numeric normalization cannot silently compare as the
same contract.
Output, file count, file size, process sampling, filesystem depth, total
runtime, idle time, and post-exit settlement are bounded by the scenario's
limits. The result marks truncated observations and residual unknowns rather
+94 -10
View File
@@ -1,13 +1,18 @@
# 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.
Prefer evidence in this order: full end-to-end workflows through the public
CLI or MCP with real production providers and no mocked dependencies,
integration across production data/API boundaries, then golden regressions
from real captured inputs. Keep a focused module test only for a distinct
failure or semantic case that stronger workflows cannot reliably reproduce.
A test's path, compiled imports, or suite name does not establish its depth.
Avoid tests that mirror getters, enum mappings, fixture helpers, or prescribed
call sequences, or assert cache object identity without a caller-visible contract.
Delete tests that only mirror getters, serialization helpers, enum mappings,
fixture helpers, prescribed call sequences, arbitrary counts or snapshots,
or cache object identity without a caller-visible contract. Do not retain a
weaker test merely because it is fast when a stronger workflow already proves
the same claim. Keep format-specific malformed representations and actual
capacity, cleanup, permission and target-identity regressions.
Prefer representative workflows over a Cartesian matrix when rows cross the
same boundary. Retain combinations that exercise a distinct interaction, such as
filtering within a full-output envelope. Keep distinct command handlers,
@@ -46,6 +51,15 @@ claim, even if a directory name calls it acceptance.
Emit a verifier's final success report only after awaited cleanup. Observe final
process lineage after closing its owned resources.
Owned JavaScript semantic graphs capture and schema-parse caller input before
returning an iterator. Whole-graph hashing, batched integrity checks and sealing
run through the application cancellation driver; only full completion grants
the private immutable-graph proof. Regression coverage checks canonical IDs,
validation diagnostics, nested caller mutations across yields, cancellation
before result validation and subsequent SDK requests. Single-file parsing,
schema cloning and canonical sorting remain synchronous phases; measure them
separately when assessing cancellation latency on real applications.
The Vitest runner releases cached native process inspectors during awaited
worker teardown. Fork termination does not run Node's normal exit hooks; a
per-file teardown would instead retire shared inspectors before later files
@@ -230,7 +244,12 @@ subsequent wire recovery and clean shutdown. Caller timeout does not interrupt
Hopper's synchronous native operation. Socket boundary tests deterministically
cover active and queued expiry, late replies and timer cleanup.
The Linux demo lane remains a separate `verify:hopper:linux` command.
The Linux demo lane remains a separate `verify:hopper:linux` command. Its
lifecycle checks attempt competing CLI launches for the active target and a
different target, requiring rejection before either can change the owning
MCP session's documents or procedures. Lease boundary tests also preserve
unresponsive or malformed live endpoints while allowing confirmed stale
sockets to recover.
`verify:hopper:fat` is a separate macOS lane requiring installed Hopper and the
existing Xcode clang/lipo toolchain. It compiles arm64/x86-64 thin executables and
@@ -308,11 +327,18 @@ prove behavior on the verified Ghidra 12.1.4 and JDK 21 build.
| Ghidra lane | Supported runner/target | Additional local tools |
| ------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `npm run verify:ghidra` | Linux x64/arm64 ELF or macOS x64/arm64 Mach-O | Host C compiler, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra:swift` | macOS x64/arm64 Mach-O | Host Swift compiler, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra:switch` | Linux x64 ELF; GCC/Clang optimized and stripped switch fixtures | GCC, Clang, GNU nm/objdump/strip, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra:aarch64-jump-table` | Any supported Ghidra host; AArch64 ELF; byte/halfword tables; host ARM64 Mach-O | Clang with AArch64 target support, Ghidra 12.1.4, and full JDK 21 |
| `npm run verify:ghidra:cross-format` | Any supported Ghidra host; also analyzes AArch64 ELF, x86-64 PE, and x86-64 Mach-O | `clang`, LLD, and `lld-link` in addition to host-lane prerequisites |
| `npm run verify:ghidra:windows` | Controlled Windows x64 with native x86-64 PE; `-- --x86` selects native x86 PE | Ghidra 12.1.4, full JDK 21, and the matching native artifact |
The optional Swift lane compiles a source-owned fixture and checks modern and
provider-demangled procedure names through real stdio MCP, with and without the
dSYM companion. It checks unfiltered, structs, and literal-name filters,
same-address alias evidence, deduplication, and explicit unresolved categories.
It does not add a Swift prerequisite to the host C fixture lane.
The host lane also verifies namespaced C++ ABI symbols and annotation identity.
That facet requires Ghidra's matching `demangler_gnu_v2_41` native component in
`GPL/DemanglerGnu/os/<platform>/` or `build/os/<platform>/`, in addition to the
@@ -437,6 +463,13 @@ Electron GUI behavior or another engine.
## Android APK analysis
The real Android lane also runs on Windows x64 with its matching bundled native
controls. It verifies bidirectional protocol input, CLI/MCP result parity,
real MCP cancellation/disconnect and Java exit after abrupt CLI owner
termination. The forced Windows exit does not exercise the POSIX SIGTERM
handler and can leave temporary workspace files. `verify:windows-native`
separately checks binary input, backpressure, EOF and pending-write job closure.
`npm run verify:android` requires an existing Java 17+ and an explicit
`REA_JADX_MCP_JAR` for jadx-headless-mcp 0.7.1. Set `REA_ANDROID_TEST_APK` to the
fixed public ApiDemos v6.0.18 fixture. Obtain both with the explicit
@@ -626,6 +659,17 @@ The dry-run option reports the chosen merge base, scope and build prerequisite
without executing tests or building. A missing Git base reports how to fetch
it or select another revision.
Source typechecking needs no compiled runtime or generated test metadata.
`npm run build:cached` compiles the CLI/MCP runtime and its packaged skill.
`npm run test:prepare` also generates the MCP contract test catalog, product
catalog, and portable managed evidence used by the complete suite. Focused
tests prepare those extra outputs only when their selected files consume them.
The MCP test catalog is JSON in `.cache/mcp-tool-catalog.json`; the tracked
test loader supplies its types without making generation a source-check
prerequisite. Run `npm run mcp-catalog:generate` to refresh it independently.
Documentation generation and managed conformance run through their separate
`docs:generate` and `evidence:generate` task graphs.
Changed selection can miss runtime registration, generated data, shell
entrypoints, bridges, or other relationships absent from the import graph.
An empty changed selection means no tests were selected, not verified
@@ -880,8 +924,48 @@ real local Codex CLI with:
npm run verify:agent
```
Its report records tool selection, repeated calls, token use, completion quality,
and handling of permissions and unknowns.
The packaged desktop and parser-comparison scenarios now assess a closed set of
known fixture claims. Each final answer must be a strict JSON manifest containing
exactly the requested claim IDs, values, Evidence IDs, and Evidence authority and
confidence. Golden values are not included in the prompt. The evaluator checks
exact values against both the fixture oracle and authenticated successful REA
results; an Evidence ID by itself is insufficient. Missing, duplicate, extra,
contradictory, incorrectly sourced, or unsupported claims fail the scenario.
The desktop rubric covers the exposed bridge API and members, renderer and main
IPC operations, and the resolved preload. It binds its Evidence to the exact
packaged artifact path and SHA-256. The parser rubric checks the precise heading
depth addition, discriminant, complete comparison counts, and the explicit limit
on runtime semantics. Its comparison must link to the two delivered source
analyses and use the requested module and export selectors. Boundary tests package
the same source-owned desktop fixture and compare the same source-owned parser
files through actual MCP tool results, then verify that fabricated answers fail.
Per-scenario `factualCorrectness` is `passed`, `failed`, or `not_assessed` within
the `configured_fixture_claims` scope. A pass establishes only the configured
claims, not unrestricted factual correctness. Native, managed, browser,
navigation-context, and address-context scenarios have no factual rubric and
remain `not_assessed`; the managed workflow requires both artifact and member
inspection to answer its type and entry-point question.
All scenarios retain routing, workflow, validation, repetition, process-exit,
and token-use gates. Configured factual scenarios additionally require a factual
pass. Their answer-text heuristics are diagnostic and do not affect the gate.
Scenarios without a rubric retain the legacy text-heuristic gate.
`answerTermCoverageMet` checks case-insensitive substrings,
`epistemicCuePresent` checks keywords, and `finalCitesEvidence` checks only an ID's
presence. These metrics can still accept fabricated prose and must not be read
as factual assessments. Set `REA_AGENT_EVAL_TRANSCRIPT_DIR` to retain complete
tool results and final answers for review.
Report schema version 3 changes the top-level `factualCorrectness` from a constant
string to an assessment summary with status, scope, and assessed/passed/failed/
not-assessed scenario counts. Scenario records include the factual assessment and
configured claim IDs. The evaluation scope is
`routing_workflow_and_configured_fixture_claims`. Update report consumers for
these changes. The narrower heuristic field names introduced in version 2 remain:
`answerHeuristicsMet`, `epistemicCuePresent`, `answerTermCoverageMet`, and
`requiredAnswerTermGroups`.
Regenerate the managed conformance manifest and Evidence completion ledger from
live verification results, or check them for drift:
+1 -2
View File
@@ -10,8 +10,7 @@ verified manifest into one caller-selected, absent directory.
For passive inspection, select `include_script_sources: true` (CLI:
`--include-script-sources`). For scenarios, select
`capture.network.response_body: true`. Save either the complete Evidence record
or its normalized result as UTF-8 JSON. The compact MCP wrapper's `evidence`
field contains the complete Evidence record.
or its normalized result as UTF-8 JSON. MCP `structuredContent` contains the complete Evidence record.
```sh
rea export-web-scripts /analysis/capture.json /analysis/exported-scripts --json
+2
View File
@@ -4,6 +4,8 @@
"src/main.ts",
"src/cli.ts",
"src/contracts/errorSchemas.ts",
"src/evaluation/AgentEvaluationFixtures.ts",
"src/application/RealHopperAssertions.ts",
"tests/processOwnershipRunner.ts"
],
"project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"],
+3 -1
View File
@@ -57,7 +57,9 @@ without this lane reports Windows native controls as unavailable.
deduplicated using Windows ordinal comparison, preserving their native Unicode
semantics and caller-selected values.
- Cleanup walks locked directory objects, removes files by handle, and unlinks
reparse entries without traversing their targets. Normal close, cancellation,
reparse entries without traversing their targets. Failed removal retains the
root and ancestor leases for retry; a partially removed runtime accepts only
cleanup, and its registry entry remains until removal succeeds. Normal close, cancellation,
startup failure, and timeout release provider resources. Abrupt owner death
kills the owned job; it can leave private runtime files for later operator
cleanup and does not claim transactional deletion after a crash.
+5 -2
View File
@@ -86,6 +86,7 @@ struct Runtime : Resource {
std::vector<Handle> parents;
Handle directory;
std::vector<Handle> immutableFiles;
bool closing = false;
// Main-thread admission is single-flight; only the accepted copy owns the
// atomic cancellation flag consumed by its async worker. ERROR_BUSY rejects
// another copy before it can reset that flag or replace pending ownership.
@@ -94,7 +95,8 @@ struct Runtime : Resource {
Runtime() : Resource(Kind::Runtime) {}
};
struct Process : Resource {
Handle job, process, stdoutRead, stderrRead;
Handle job, process, stdoutRead, stderrRead, stdinWrite;
bool inputPending = false;
DWORD pid = 0;
Process() : Resource(Kind::Process) {}
};
@@ -122,7 +124,8 @@ void closeRuntime(Runtime& runtime);
napi_value filesystemCall(napi_env env, const std::wstring& operation,
const std::vector<napi_value>& args);
std::unique_ptr<Process> spawnProcess(const std::wstring& command, const std::wstring& commandLine,
const std::wstring& cwd, const std::vector<std::wstring>& environment);
const std::wstring& cwd, const std::vector<std::wstring>& environment,
bool protocolStdin = false);
napi_value processCall(napi_env env, const std::wstring& operation,
const std::vector<napi_value>& args);
napi_value inspect(napi_env env);
+6 -4
View File
@@ -328,14 +328,11 @@ static void disposeSnapshot(Runtime& root, Handle& file, const std::wstring& pat
void closeRuntime(Runtime& root) {
if (root.closed) return;
root.closing = true;
// Windows keeps the bearer descriptor and snapshot leases until this owner
// cleanup. POSIX removes its consumed descriptor earlier. Abrupt owner death
// closes process jobs but cannot run this walk, so private files can remain.
root.immutableFiles.clear();
struct ReleaseRuntime {
Runtime& root;
~ReleaseRuntime() { root.directory.reset(); root.parents.clear(); root.closed = true; }
} release{root};
// Each traversed directory stays locked against replacement. Reparse entries
// are unlinked as objects; their targets are never enumerated or deleted.
struct Frame {
@@ -380,6 +377,10 @@ void closeRuntime(Runtime& root) {
frames.push_back(std::make_unique<Frame>(std::move(child), path));
else dispose(child.get(), path);
}
// Failed walks retain the root's identity leases for a later cleanup retry.
root.directory.reset();
root.parents.clear();
root.closed = true;
}
struct SnapshotWork {
@@ -528,6 +529,7 @@ napi_value filesystemCall(napi_env env, const std::wstring& operation, const std
return result;
}
auto& root = static_cast<Runtime&>(resource(env, args[0], Kind::Runtime));
require(!root.closing || operation == L"runtime_close", "Runtime cleanup is pending", root.path, ERROR_BUSY);
if (operation == L"runtime_snapshot_cancel") { root.snapshotCancelled.store(true); return null(env); }
require(!root.snapshotPending, "Runtime snapshot is still pending", root.path, ERROR_BUSY);
// Reject overlapping operations before snapshot() resets cancellation. The
+100 -9
View File
@@ -34,16 +34,17 @@ static Handle createJob() {
return job;
}
static void pipe(Handle& read, Handle& write) {
static void pipe(Handle& read, Handle& write, bool parentReads = true) {
SECURITY_ATTRIBUTES attributes{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
HANDLE rawRead, rawWrite;
require(CreatePipe(&rawRead, &rawWrite, &attributes, 0), "Create owned process output pipe failed");
read.reset(rawRead); write.reset(rawWrite);
require(SetHandleInformation(read.get(), HANDLE_FLAG_INHERIT, 0), "Protect process output handle inheritance failed");
require(SetHandleInformation(parentReads ? read.get() : write.get(), HANDLE_FLAG_INHERIT, 0), "Protect parent process pipe handle inheritance failed");
}
std::unique_ptr<Process> spawnProcess(const std::wstring& command, const std::wstring& commandLine,
const std::wstring& cwd, const std::vector<std::wstring>& environment) {
const std::wstring& cwd, const std::vector<std::wstring>& environment,
bool protocolStdin) {
const auto executable = ordinaryDriveSeparators(command);
require(executable.size() >= 3 && executable[1] == L':' && executable[2] == L'\\',
"Owned Windows process requires an absolute executable path", command, ERROR_INVALID_PARAMETER);
@@ -52,9 +53,13 @@ std::unique_ptr<Process> spawnProcess(const std::wstring& command, const std::ws
Handle stdoutWrite, stderrWrite;
pipe(result->stdoutRead, stdoutWrite); pipe(result->stderrRead, stderrWrite);
SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
Handle input(CreateFileW(L"NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
&security, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
require(input.valid(), "Open owned process null input failed");
Handle input;
if (protocolStdin) pipe(input, result->stdinWrite, false);
else {
input.reset(CreateFileW(L"NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
&security, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
require(input.valid(), "Open owned process null input failed");
}
Attributes attributes(2);
HANDLE job = result->job.get();
attributes.add(PROC_THREAD_ATTRIBUTE_JOB_LIST, &job, sizeof(job));
@@ -198,16 +203,93 @@ static napi_value callerToken(napi_env env) {
return result;
}
struct InputWork {
napi_async_work work = nullptr;
napi_deferred deferred = nullptr;
napi_ref processReference = nullptr;
Process* process;
Handle pipe;
std::vector<unsigned char> bytes;
size_t written = 0;
std::unique_ptr<Failure> failure;
explicit InputWork(Process& process) : process(&process) {}
};
static void writeInput(napi_env, void* pointer) {
auto& work = *static_cast<InputWork*>(pointer);
try {
// Anonymous pipe writes may block. Keep them off the JS thread so output
// polling and job termination remain live while a child stops reading.
while (work.written < work.bytes.size()) {
DWORD written = 0;
const auto count = static_cast<DWORD>(std::min<size_t>(65536, work.bytes.size() - work.written));
require(WriteFile(work.pipe.get(), work.bytes.data() + work.written, count, &written, nullptr),
"Write owned process input failed");
require(written > 0, "Owned process input write made no progress", L"", ERROR_WRITE_FAULT);
work.written += written;
}
} catch (const Failure& failure) {
work.failure = std::make_unique<Failure>(failure);
} catch (const std::exception& failure) {
work.failure = std::make_unique<Failure>(failure.what(), L"", ERROR_GEN_FAILURE);
}
}
static void completeInput(napi_env env, napi_status status, void* pointer) {
std::unique_ptr<InputWork> work(static_cast<InputWork*>(pointer));
work->process->inputPending = false;
if (work->failure) napi_reject_deferred(env, work->deferred, failureValue(env, *work->failure));
else if (status != napi_ok)
napi_reject_deferred(env, work->deferred, failureValue(env, Failure("Owned process input worker was cancelled", L"", ERROR_OPERATION_ABORTED)));
else napi_resolve_deferred(env, work->deferred, number(env, work->written));
napi_delete_reference(env, work->processReference);
napi_delete_async_work(env, work->work);
}
static napi_value inputWrite(napi_env env, napi_value owner, Process& process, napi_value input) {
require(process.stdinWrite.valid(), "Owned process has no writable input", L"", ERROR_BROKEN_PIPE);
require(!process.inputPending, "Owned process input already has a pending write", L"", ERROR_BUSY);
auto work = std::make_unique<InputWork>(process);
void* bytes = nullptr;
size_t size = 0;
bool buffer = false;
check(napi_is_buffer(env, input, &buffer));
require(buffer, "Owned process input must be a Buffer", L"", ERROR_INVALID_PARAMETER);
check(napi_get_buffer_info(env, input, &bytes, &size));
if (size > 0) work->bytes.assign(static_cast<unsigned char*>(bytes), static_cast<unsigned char*>(bytes) + size);
HANDLE pipe;
require(DuplicateHandle(GetCurrentProcess(), process.stdinWrite.get(), GetCurrentProcess(), &pipe,
0, FALSE, DUPLICATE_SAME_ACCESS), "Retain owned process input handle failed");
work->pipe.reset(pipe);
napi_value promise;
check(napi_create_promise(env, &work->deferred, &promise));
check(napi_create_reference(env, owner, 1, &work->processReference));
try {
check(napi_create_async_work(env, nullptr, string(env, std::string("REA owned process input")),
writeInput, completeInput, work.get(), &work->work));
check(napi_queue_async_work(env, work->work));
process.inputPending = true;
} catch (...) {
if (work->work) napi_delete_async_work(env, work->work);
napi_delete_reference(env, work->processReference);
throw;
}
work.release();
return promise;
}
napi_value processCall(napi_env env, const std::wstring& operation, const std::vector<napi_value>& args) {
if (operation == L"process_caller_token") {
require(args.empty(), "Caller token observation takes no arguments", L"", ERROR_INVALID_PARAMETER);
return callerToken(env);
}
if (operation == L"process_spawn") {
require(args.size() == 4, "Wrong native process launch argument count", L"", ERROR_INVALID_PARAMETER);
require(args.size() == 4 || args.size() == 5, "Wrong native process launch argument count", L"", ERROR_INVALID_PARAMETER);
bool protocolStdin = false;
if (args.size() == 5) check(napi_get_value_bool(env, args[4], &protocolStdin));
std::vector<std::wstring> environment;
for (const auto& entry : array(env, args[3])) environment.push_back(wide(env, entry));
auto process = spawnProcess(wide(env, args[0]), wide(env, args[1]), wide(env, args[2]), environment);
auto process = spawnProcess(wide(env, args[0]), wide(env, args[1]), wide(env, args[2]), environment, protocolStdin);
auto result = object(env);
set(env, result, "pid", number(env, process->pid));
set(env, result, "jobAssigned", boolean(env, true));
@@ -215,8 +297,16 @@ napi_value processCall(napi_env env, const std::wstring& operation, const std::v
set(env, result, "handle", wrap(env, std::move(process)));
return result;
}
if (operation == L"process_stdin_write") {
require(args.size() == 2, "Wrong native process input argument count", L"", ERROR_INVALID_PARAMETER);
return inputWrite(env, args[0], static_cast<Process&>(resource(env, args[0], Kind::Process)), args[1]);
}
require(args.size() == 1, "Wrong native process handle argument count", L"", ERROR_INVALID_PARAMETER);
auto& process = static_cast<Process&>(resource(env, args[0], Kind::Process));
if (operation == L"process_stdin_close") {
process.stdinWrite.reset();
return null(env);
}
if (operation == L"process_poll") {
auto result = object(env);
bool stdoutEnded = false, stderrEnded = false;
@@ -240,7 +330,7 @@ napi_value processCall(napi_env env, const std::wstring& operation, const std::v
}
if (operation == L"process_close") {
// Closing the job is the crash-safe authority; no PID enumeration is used.
process.job.reset(); process.process.reset(); process.stdoutRead.reset(); process.stderrRead.reset();
process.job.reset(); process.process.reset(); process.stdoutRead.reset(); process.stderrRead.reset(); process.stdinWrite.reset();
process.closed = true;
return null(env);
}
@@ -266,6 +356,7 @@ napi_value inspect(napi_env env) {
set(env, result, "privateDacl", boolean(env, true));
set(env, result, "atomicJobAssignment", boolean(env, true));
set(env, result, "killOnOwnerClose", boolean(env, true));
set(env, result, "protocolStdin", boolean(env, true));
return result;
}
+37 -28
View File
@@ -80,27 +80,26 @@
],
"scripts": {
"deps:check": "node scripts/check-dependency-install.mjs",
"metadata:generate": "node scripts/generate-package-metadata.mjs",
"metadata:generate": "node scripts/run-exclusive.mjs artifacts node scripts/generate-package-metadata.mjs",
"metadata:check": "node scripts/generate-package-metadata.mjs --check",
"mcp-registry:check": "node scripts/verify-mcp-registry-metadata.mjs",
"mcp-catalog:generate": "npm run build:cached",
"mcp-catalog:check": "node scripts/generate-mcp-tool-catalog.mjs --check",
"prebuild": "npm run deps:check && npm run metadata:check",
"build": "node scripts/run-exclusive.mjs build npm run build:unlocked",
"build:unlocked": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json && 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",
"mcp-catalog:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:mcp-catalog",
"mcp-catalog:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run mcp-catalog:check:built",
"prebuild": "npm run deps:check",
"build": "node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills --force",
"precheck": "npm run deps:check",
"check": "turbo run typecheck lint format:check knip metadata:check",
"check": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint format:check knip metadata:check",
"precheck:fast": "npm run deps:check",
"check:fast": "turbo run typecheck lint --output-logs=errors-only",
"precheck:changed": "npm run deps:check",
"check:changed": "npm run check:fast && npm run test:changed",
"precheck:test": "npm run deps:check",
"check:test": "turbo run typecheck lint format:check knip metadata:check test:run",
"check:test": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint format:check knip metadata:check test:run",
"precheck:pr": "npm run deps:check",
"check:pr": "turbo run typecheck lint format:check knip metadata:check test:run && npm run docs:check && npm run jscpd && npm run scan:todos",
"check:pr": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint format:check knip metadata:check test:run && npm run docs:check && npm run jscpd && npm run scan:todos",
"precheck:ci": "npm run deps:check",
"check:ci": "turbo run typecheck lint format:check knip metadata:check jscpd scan:todos",
"build:cached": "node scripts/check-dependency-install.mjs && turbo run build",
"check:ci": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint format:check knip metadata:check jscpd scan:todos",
"build:cached": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills",
"format": "oxfmt .",
"format:check": "oxfmt --check .",
"lint": "oxlint && npm run verify:module-boundaries",
@@ -110,33 +109,33 @@
"scan:todos": "node scripts/scan-todos.mjs",
"start": "node scripts/rea.mjs mcp",
"test": "npm run test:with-build",
"test:with-build": "npm run build:cached && vitest run",
"test:with-build": "npm run test:prepare && vitest run",
"test:run": "vitest run",
"test:local": "node scripts/run-development-tests.mjs local",
"test:fast": "vitest run --project domain --project services --project adapters --project composition --project conformance --project evaluation",
"pretest:boundary": "npm run build:cached",
"pretest:boundary": "npm run test:prepare",
"test:boundary": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global",
"pretest:mcp": "npm run build:cached",
"pretest:mcp": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills artifacts:mcp-catalog",
"test:mcp": "vitest run --project mcp-boundary",
"pretest:acceptance": "npm run build:cached",
"test:acceptance": "vitest run --project acceptance",
"test:changed": "node scripts/run-development-tests.mjs changed",
"pretest:integration": "npm run build:cached",
"pretest:integration": "npm run test:prepare",
"test:integration": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global --project acceptance",
"pretest:coverage": "npm run build:cached",
"pretest:coverage": "npm run test:prepare",
"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",
"pretest:ci:shard": "npm run test:prepare",
"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",
"pretest:verbose": "npm run build:cached",
"pretest:verbose": "npm run test:prepare",
"test:verbose": "vitest run --reporter=verbose",
"test:watch": "vitest --project domain --project services --project adapters --project composition --project conformance --project evaluation --changed",
"pretest:watch:all": "npm run build:cached",
"pretest:watch:all": "npm run test:prepare",
"test:watch:all": "vitest --changed",
"typecheck": "tsc --noEmit",
"verify:test-temp-hygiene": "npm run build:cached && node scripts/verify-test-temp-hygiene.mjs",
"verify:test-temp-hygiene": "npm run test:prepare && node scripts/verify-test-temp-hygiene.mjs",
"verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs",
"verify:hopper:deadlines": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper-deadlines.mjs",
"verify:hopper:fat": "npm run build:cached && node scripts/verify-real-hopper-fat.mjs",
@@ -146,6 +145,7 @@
"verify:keyed-archive": "npm run build:cached && node scripts/verify/apple/keyed-archive.mjs",
"verify:macos-bundle": "npm run build:cached && node scripts/verify/apple/macos-bundle.mjs",
"verify:ghidra": "npm run build:cached && node scripts/verify-real-ghidra.mjs",
"verify:ghidra:swift": "npm run build:cached && node scripts/verify-real-ghidra-swift.mjs",
"verify:ida": "npm run build:cached && node scripts/verify-real-ida.mjs",
"verify:ghidra:switch": "npm run build:cached && node scripts/verify-real-ghidra-switch.mjs",
"verify:ghidra:dos": "npm run build:cached && node scripts/verify-real-ghidra-dos.mjs",
@@ -180,18 +180,17 @@
"probe:windows-capabilities": "npm run build:cached && node scripts/windows-capabilities.mjs",
"verify:published": "node scripts/verify-published-package.mjs",
"config:print": "node scripts/print-mcp-config.mjs",
"evidence:generate": "npm run build:cached && node scripts/generate-completion-ledger.mjs",
"evidence:check": "npm run build:cached && node scripts/generate-completion-ledger.mjs --check",
"evidence:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:managed-evidence",
"evidence:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run evidence:check:built",
"predocs:dev": "npm run docs:generate",
"docs:dev": "vitepress dev docs",
"predocs:build": "npm run docs:generate",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs",
"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-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",
"docs:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts npm run docs:prepare",
"docs:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts 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:error-schema": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:error-schema",
"prepack": "node scripts/prepack.mjs",
"prepare": "node scripts/prepare.mjs",
"verify:asset-catalog": "npm run build:cached && node scripts/verify/apple/asset-catalog.mjs",
@@ -216,7 +215,17 @@
"verify:web:network-captures": "npm run build:cached && node scripts/verify-web-network-captures.mjs",
"verify:binary:layout": "npm run build:cached && node scripts/verify-binary-layout.mjs",
"verify:evm:interface": "npm run build:cached && node scripts/verify-evm-interface.mjs",
"verify:recorded:crash": "npm run build:cached && node scripts/verify-recorded-crash.mjs"
"verify:recorded:crash": "npm run build:cached && node scripts/verify-recorded-crash.mjs",
"mcp-catalog:check:built": "node scripts/generate-mcp-tool-catalog.mjs --check",
"compile": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json",
"artifacts:mcp-catalog": "node scripts/generate-mcp-tool-catalog.mjs",
"artifacts:skills": "node scripts/generate-skill-metadata.mjs",
"artifacts:product-catalog": "node scripts/generate-product-catalog.mjs",
"artifacts:managed-evidence": "node scripts/generate-completion-ledger.mjs",
"artifacts:error-schema": "node scripts/generate-error-schema.mjs",
"test:prepare": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:mcp-catalog artifacts:product-catalog artifacts:managed-evidence",
"evidence:check:built": "node scripts/generate-completion-ledger.mjs --check",
"docs:prepare": "node scripts/generate-package-metadata.mjs && turbo run artifacts:product-catalog artifacts:managed-evidence artifacts:error-schema"
},
"lint-staged": {
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
+1 -4
View File
@@ -4,7 +4,4 @@ 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 }),
]);
await rm(join(root, "dist"), { recursive: true, force: true });
+9 -103
View File
@@ -1,13 +1,12 @@
import { mkdir } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { formatGeneratedFile } from "./lib/format-generated-file.mjs";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { Ajv2020 } from "ajv/dist/2020.js";
import { McpServer } from "@modelcontextprotocol/server";
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managed/managedWorkflowToolContracts.js";
import { auxiliaryAnalysisProviderDeclarations } from "../dist/composition/auxiliaryAnalysisProviders.js";
import { toolRegistrationOptions } from "../dist/server/toolRegistrationOptions.js";
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
@@ -17,10 +16,9 @@ for (const argument of arguments_)
throw new Error(`Unknown MCP tool catalog option: ${argument}`);
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
// This build-generated source supports schema and provider conformance tests;
// This generated metadata supports schema conformance tests;
// production MCP tools are registered from their canonical contracts directly.
// Keep the test metadata portable and regenerate it with `npm run build`.
const CATALOG_PLATFORM = "darwin";
// Keep the test metadata portable and regenerate it with `npm run mcp-catalog:generate`.
const sessionToolNames = new Set([
...TOOL_CONTRACTS.filter(({ kind }) => kind === "session").map(
({ name }) => name,
@@ -87,10 +85,6 @@ async function sdkToolCatalog() {
await Promise.allSettled([client.close(), server.close()]);
}
}
const auxiliaryProviders = auxiliaryAnalysisProviderDeclarations(
CATALOG_PLATFORM,
).map(({ identity, capabilities }) => ({ identity, capabilities }));
const canonicalizeJson = (value) => {
if (Array.isArray(value)) return value.map(canonicalizeJson);
if (value === null || typeof value !== "object") return value;
@@ -101,101 +95,13 @@ const canonicalizeJson = (value) => {
);
};
const payloadJson = JSON.stringify(
canonicalizeJson({ catalog, auxiliaryProviders }),
null,
2,
);
const source = await formatGeneratedFile(
"src/generatedMcpToolCatalog.ts",
`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);
}
`,
);
const payloadJson = JSON.stringify(canonicalizeJson({ catalog }), null, 2);
const outputPath = join(root, ".cache/mcp-tool-catalog.json");
if (!arguments_.has("--check"))
await mkdir(dirname(outputPath), { recursive: true });
await ensureGeneratedFile({
path: join(root, "src/generatedMcpToolCatalog.ts"),
source,
path: outputPath,
source: `${payloadJson}\n`,
check: arguments_.has("--check"),
generateCommand: "npm run mcp-catalog:generate",
});
+5 -2
View File
@@ -75,6 +75,9 @@ export async function withArtifactMcp(target, verify) {
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;
assert.ok(
result.structuredContent?.normalized_result,
"MCP omitted structured result",
);
return result.structuredContent.normalized_result;
}
+1 -2
View File
@@ -48,8 +48,7 @@ export async function verifyBrowserDomDestinations(
});
assert.notEqual(result.isError, true, mcpTextValue(result));
assertFormDestinations(
parseEvidence(JSON.parse(mcpTextValue(result)).evidence)
.normalized_result,
parseEvidence(JSON.parse(mcpTextValue(result))).normalized_result,
documentUrl,
);
return { cli: true, stdio_mcp: true, current_document_destinations: 4 };
+3 -5
View File
@@ -105,9 +105,7 @@ export async function verifyBrowserModules(
arguments: input,
});
assert.notEqual(response.isError, true, mcpTextValue(response));
const mcpEvidence = parseEvidence(
JSON.parse(mcpTextValue(response)).evidence,
);
const mcpEvidence = parseEvidence(JSON.parse(mcpTextValue(response)));
const mcpResult = assertTrace(mcpEvidence, oracle, site, scriptIndex);
assert.deepEqual(mcpResult, cliResult, "CLI and MCP module traces differ");
assert.equal(
@@ -143,7 +141,7 @@ export async function verifyBrowserModules(
},
}),
),
).evidence,
),
);
const result = webModuleTraceResultSchema.parse(
withoutMap.normalized_result,
@@ -236,7 +234,7 @@ export async function verifyBrowserModules(
},
});
assert.notEqual(response.isError, true, mcpTextValue(response));
evidence = parseEvidence(JSON.parse(mcpTextValue(response)).evidence);
evidence = parseEvidence(JSON.parse(mcpTextValue(response)));
}
const result = webModuleTraceResultSchema.parse(
evidence.normalized_result,
+2 -8
View File
@@ -112,10 +112,7 @@ export async function verifyBrowserNetworkEvidence(
{ timeout: 60_000 },
);
assert.notEqual(result.isError, true, JSON.stringify(result));
const evidence = parseEvidence({
...result.structuredContent?.evidence,
normalized_result: result.structuredContent?.result,
});
const evidence = parseEvidence(result.structuredContent);
mcpProof = assertBrowserNetworkEvidence(
browserScenarioCaptureSchema.parse(evidence.normalized_result),
site.origin,
@@ -130,10 +127,7 @@ export async function verifyBrowserNetworkEvidence(
{ timeout: 60_000 },
);
assert.notEqual(connected.isError, true, JSON.stringify(connected));
const connectedEvidence = parseEvidence({
...connected.structuredContent?.evidence,
normalized_result: connected.structuredContent?.result,
});
const connectedEvidence = parseEvidence(connected.structuredContent);
assert.deepEqual(
assertBrowserNetworkEvidence(
browserScenarioCaptureSchema.parse(
+5 -8
View File
@@ -8,7 +8,7 @@ import { chromium } from "playwright-core";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { PlaywrightBrowserScenarioProvider } from "../../dist/browser/PlaywrightBrowserScenarioProvider.js";
import { createBrowserScenarioProvider } from "../../dist/composition/browserScenario.js";
import { PlaywrightScenarioEvents } from "../../dist/browser/PlaywrightScenarioEvents.js";
import { BrowserScenarioSecrets } from "../../dist/browser/BrowserScenarioSecrets.js";
import { browserScenarioSchema } from "../../dist/domain/browserScenario.js";
@@ -53,9 +53,9 @@ const scenarioFor = (executable, origin, events) =>
});
const capture = async (scenario) => {
const result = await new PlaywrightBrowserScenarioProvider().captureScenario(
scenario,
);
const result = await createBrowserScenarioProvider(
process.env,
).captureScenario(scenario);
if (!result.ok) throw result.error;
assert.ok(
result.value.steps.every((step) => step.status === "completed"),
@@ -163,10 +163,7 @@ const verifyTransports = async (executable, origin) => {
{ timeout: 60_000 },
);
assert.notEqual(captured.isError, true, JSON.stringify(captured));
const evidence = parseEvidence({
...captured.structuredContent?.evidence,
normalized_result: captured.structuredContent?.result,
});
const evidence = parseEvidence(captured.structuredContent);
assertNetwork(
browserScenarioCaptureSchema.parse(evidence.normalized_result),
origin,
+2 -2
View File
@@ -126,7 +126,7 @@ export async function verifyBrowserRuntime(
mcpTextValue(listenerResponse),
);
const mcpListenerProof = assertListeners(
parseEvidence(JSON.parse(mcpTextValue(listenerResponse)).evidence),
parseEvidence(JSON.parse(mcpTextValue(listenerResponse))),
site,
);
const mcpProof = await executionMcp(
@@ -231,7 +231,7 @@ async function executionMcp(
await actionPromise;
assert.notEqual(response.isError, true, mcpTextValue(response));
return assertExecution(
parseEvidence(JSON.parse(mcpTextValue(response)).evidence),
parseEvidence(JSON.parse(mcpTextValue(response))),
site,
requireBlock,
);
+1 -4
View File
@@ -94,10 +94,7 @@ export async function verifyLargeScreenshotE2e(endpoint, origin) {
{ timeout: 60000 },
);
assert.notEqual(captured.isError, true, JSON.stringify(captured));
const mcpEvidence = parseEvidence({
...captured.structuredContent?.evidence,
normalized_result: captured.structuredContent?.result,
});
const mcpEvidence = parseEvidence(captured.structuredContent);
const mcpScreenshot = webScreenshotSchema.parse(
mcpEvidence.normalized_result,
);
+1 -1
View File
@@ -69,7 +69,7 @@ export async function verifyBrowserScriptExport(
const call = async (name, args) => {
const result = await client.callTool({ name, arguments: args });
assert.notEqual(result.isError, true, mcpTextValue(result));
return parseEvidence(JSON.parse(mcpTextValue(result)).evidence);
return parseEvidence(JSON.parse(mcpTextValue(result)));
};
const mcpCapture = await call("capture_browser_scenario", scenario);
const mcpInput = await save("mcp", mcpCapture);
+1 -3
View File
@@ -184,9 +184,7 @@ async function assertPublicPair(cli, client, oracle) {
arguments: input,
});
assert.notEqual(response.isError, true, mcpTextValue(response));
const mcpEvidence = parseEvidence(
JSON.parse(mcpTextValue(response)).evidence,
);
const mcpEvidence = parseEvidence(JSON.parse(mcpTextValue(response)));
assert.deepEqual(assertPoint(mcpEvidence, oracle), expected);
assert.equal(evidence.parameters.source_map.url, input.source_map.url);
return expected;
+9 -6
View File
@@ -75,6 +75,7 @@ export const toolFamilyCatalog = (sources) => {
surface: "application-workflow",
contracts: [
...sources.applicationContracts.APPLICATION_TOOL_CONTRACTS,
...sources.analysisViewContracts.ANALYSIS_VIEW_TOOL_CONTRACTS,
...sources.webScriptContracts.WEB_SCRIPT_TOOL_CONTRACTS,
...sources.javascriptRecoveryContracts
.JAVASCRIPT_RECOVERY_TOOL_CONTRACTS,
@@ -199,7 +200,7 @@ export const providerCatalog = (sources) => {
sources.webNetworkCaptureContracts.WEB_NETWORK_CAPTURE_TOOL_CONTRACTS,
},
{
identity: sources.browserProvider.CDP_BROWSER_PROVIDER_IDENTITY,
identity: sources.browserIdentities.CDP_BROWSER_PROVIDER_IDENTITY,
contracts: [
...sources.browserContracts.BROWSER_TOOL_CONTRACTS,
...sources.webRuntimeContracts.WEB_RUNTIME_TOOL_CONTRACTS,
@@ -207,19 +208,17 @@ export const providerCatalog = (sources) => {
},
{
identity:
sources.browserScenarioProvider
.PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY,
sources.browserIdentities.PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY,
contracts:
sources.browserScenarioContracts.BROWSER_SCENARIO_TOOL_CONTRACTS,
},
{
identity: sources.electronProvider.CDP_ELECTRON_PROVIDER_IDENTITY,
identity: sources.browserIdentities.CDP_ELECTRON_PROVIDER_IDENTITY,
contracts: observationContracts,
},
{
identity:
sources.electronActiveProvider
.PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY,
sources.browserIdentities.PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY,
contracts: activeContracts,
},
{
@@ -246,6 +245,10 @@ export const providerCatalog = (sources) => {
name !== "project_apple_application_graph",
),
},
{
identity: sources.artifactProviders.ANALYSIS_VIEW_PROVIDER,
contracts: sources.analysisViewContracts.ANALYSIS_VIEW_TOOL_CONTRACTS,
},
{
identity: sources.artifactProviders.ANDROID_APPLICATION_PROVIDER,
contracts: sources.applicationContracts.APPLICATION_TOOL_CONTRACTS.filter(
+2 -4
View File
@@ -64,6 +64,7 @@ const SOURCE_PATHS = {
javascriptRuntimeObservationContracts:
"dist/contracts/javascript/javascriptRuntimeObservationToolContracts.js",
applicationContracts: "dist/contracts/applicationToolContracts.js",
analysisViewContracts: "dist/contracts/analysisViewToolContracts.js",
webScriptContracts: "dist/contracts/webScriptToolContracts.js",
javascriptRecoveryContracts:
"dist/contracts/javascript/javascriptRecoveryToolContracts.js",
@@ -74,10 +75,7 @@ const SOURCE_PATHS = {
idaProvider: "dist/ida/IdaProviderCapabilities.js",
nativeProvider: "dist/native/NativeMacOSProviderMetadata.js",
artifactProviders: "dist/application/InvestigationProviders.js",
browserProvider: "dist/browser/CdpBrowserProvider.js",
browserScenarioProvider: "dist/browser/PlaywrightBrowserScenarioProvider.js",
electronProvider: "dist/browser/CdpElectronProvider.js",
electronActiveProvider: "dist/browser/PlaywrightElectronActiveProvider.js",
browserIdentities: "dist/browser/providerIdentities.js",
v8InspectorProvider: "dist/inspector/V8InspectorProvider.js",
evidence: "dist/domain/evidence.js",
evidenceBundle: "dist/domain/evidenceBundle.js",
+2 -1
View File
@@ -11,11 +11,12 @@ export function parseDevelopmentTestRequest(
arguments_: readonly string[],
): DevelopmentTestRequest;
/** Plan source feedback or a strict explicit run with its build prerequisite. */
/** Plan source feedback or explicit tests with runtime/artifact prerequisites. */
export function developmentTestPlan(
request: DevelopmentTestRequest,
baseCommit?: string,
): {
readonly needsBuild: boolean;
readonly artifactTasks: readonly string[];
readonly vitestArguments: readonly string[];
};
+18 -1
View File
@@ -49,7 +49,20 @@ export const parseDevelopmentTestRequest = (mode, arguments_) => {
return { mode, paths: [...new Set(paths)], base, dryRun };
};
/** Plan source feedback or a strict explicit run with its build prerequisite. */
// Test-only outputs are declared beside the test planner rather than becoming
// runtime or typecheck prerequisites. Add a consumer here when it reads one.
const artifactConsumers = {
"artifacts:mcp-catalog": [
"tests/boundary/mcp/contractPresentation.test.ts",
"tests/boundary/mcp/toolSchemaValidity.test.ts",
],
"artifacts:product-catalog": ["tests/boundary/cli/productCatalog.test.ts"],
"artifacts:managed-evidence": [
"tests/boundary/filesystem/setupSkill.test.ts",
],
};
/** Plan source feedback or explicit tests with their runtime/artifact prerequisites. */
export const developmentTestPlan = (request, baseCommit) => {
const explicit = request.paths.length > 0;
if (
@@ -60,6 +73,10 @@ export const developmentTestPlan = (request, baseCommit) => {
const projects = request.mode === "focused" ? [] : sourceProjects;
return {
needsBuild: request.paths.some((path) => !sourceTest(path)),
artifactTasks: Object.entries(artifactConsumers).flatMap(
([task, consumers]) =>
request.paths.some((path) => consumers.includes(path)) ? [task] : [],
),
vitestArguments: [
"run",
...projects.flatMap((project) => ["--project", project]),
@@ -15,14 +15,17 @@ import {
} from "../../dist/domain/analysisSnapshot.js";
import { createEvidence } from "../../dist/domain/evidence.js";
import { createEvidenceBundle } from "../../dist/domain/evidenceBundle.js";
import { requireMcpResult } from "./mcp-verifier-results.mjs";
import {
requireMcpLifecycleResult,
requireMcpEvidenceResult,
} from "./mcp-verifier-results.mjs";
/** Reject a valid older snapshot whose instruction-only collector omitted a real edge. */
export async function verifyLegacyGhidraReferenceSnapshot(
client,
{ target, procedure, omittedEdge, entrypoint, env },
) {
const bundle = requireMcpResult(
const bundle = requireMcpLifecycleResult(
await client.callTool({
name: "get_evidence_bundle",
arguments: {},
@@ -97,6 +100,7 @@ export async function verifyLegacyGhidraReferenceSnapshot(
},
}),
],
workflow_entries: [],
evidence_bundle: createEvidenceBundle([evidence]),
});
assert.equal(
@@ -107,7 +111,7 @@ export async function verifyLegacyGhidraReferenceSnapshot(
const path = join(env.TMPDIR, `legacy-reference-${randomUUID()}.json`);
await writeFile(path, serializeAnalysisSnapshot(snapshot), { flag: "wx" });
try {
const session = requireMcpResult(
const session = requireMcpLifecycleResult(
await client.callTool({ name: "binary_session", arguments: {} }),
"binary_session",
);
@@ -128,14 +132,14 @@ export async function verifyLegacyGhidraReferenceSnapshot(
/without this snapshot.*fresh snapshot/u,
);
assert.deepEqual(
requireMcpResult(
requireMcpLifecycleResult(
await client.callTool({ name: "binary_session", arguments: {} }),
"binary_session",
),
session,
"A rejected snapshot must preserve the selected session",
);
const retained = requireMcpResult(
const retained = requireMcpEvidenceResult(
await client.callTool({
name: "analyze_function",
arguments: { procedure },
+49 -14
View File
@@ -1,3 +1,5 @@
import { toolContract } from "../../dist/contracts/toolContracts.js";
/** Read the textual JSON projection from one MCP tool call. */
export const mcpTextValue = (result) => {
const text = result.content?.find((item) => item.type === "text")?.text;
@@ -7,25 +9,58 @@ export const mcpTextValue = (result) => {
const jsonValue = (result) => JSON.parse(mcpTextValue(result));
/** Return the compact result projection from one successful MCP tool call. */
export const requireMcpResult = (result, operation) => {
if (result.isError === true) {
const requireOutput = (result, operation, contract) => {
if (result.isError === true)
throw new Error(`${operation} failed: ${mcpTextValue(result)}`);
}
const value = jsonValue(result);
if (value === null || typeof value !== "object" || !("result" in value)) {
throw new Error(`${operation} omitted its result projection`);
}
return value.result;
if (contract.name !== operation)
throw new Error(`${operation} received the ${contract.name} contract`);
return contract.outputSchema.parse(jsonValue(result));
};
const operationResultKey = (contract) => {
const shape = contract.outputSchema.shape;
if (shape === undefined)
throw new Error(`${contract.name} has an unsupported MCP output schema`);
const evidence = Object.hasOwn(shape, "normalized_result");
const lifecycle = Object.hasOwn(shape, "result");
if (evidence === lifecycle)
throw new Error(`${contract.name} has an unsupported MCP output schema`);
return evidence ? "normalized_result" : "result";
};
/** Validate an Evidence-producing tool output and return its normalized result. */
export const requireMcpEvidenceResult = (result, operation) => {
const contract = toolContract(operation);
if (operationResultKey(contract) !== "normalized_result")
throw new Error(`${operation} requires the lifecycle result extractor`);
return requireOutput(result, operation, contract).normalized_result;
};
/** Validate a lifecycle tool output and return its operation result. */
export const requireMcpLifecycleResult = (result, operation) => {
const contract = toolContract(operation);
if (operationResultKey(contract) !== "result")
throw new Error(`${operation} requires the Evidence result extractor`);
return requireOutput(result, operation, contract).result;
};
/** Project a mixed verifier call using its exact named canonical output schema. */
export const requireMcpOperationResult = (
result,
operation,
contract = toolContract(operation),
) => {
const key = operationResultKey(contract);
return requireOutput(result, operation, contract)[key];
};
/** Verify provider identity directly from the tool result. */
export const requireEvidenceProvider = (result, operation, providerId) => {
const evidence = jsonValue(result);
if (
evidence?.evidence?.provider?.id !== providerId ||
evidence?.evidence?.analysis_profile?.provider?.id !== providerId ||
typeof evidence.evidence.analysis_profile.provider.version !== "string"
evidence?.provider?.id !== providerId ||
evidence?.analysis_profile?.provider?.id !== providerId ||
typeof evidence.analysis_profile.provider.version !== "string"
) {
throw new Error(`${operation} omitted its concrete provider provenance`);
}
@@ -38,10 +73,10 @@ export const requireWorkflowEvidenceProvider = (
expected,
) => {
const evidence = jsonValue(result);
const profile = evidence?.evidence?.analysis_profile;
const profile = evidence?.analysis_profile;
const upstream = profile?.parameters?.upstream_analysis_profile;
if (
evidence?.evidence?.provider?.id !== expected.workflowProviderId ||
evidence?.provider?.id !== expected.workflowProviderId ||
profile?.provider?.id !== expected.workflowProviderId ||
upstream?.provider?.id !== expected.upstreamProviderId ||
typeof upstream.provider.version !== "string"
-13
View File
@@ -14,14 +14,6 @@ const MIGRATED_PROVIDER_ROOTS = new Set([
"inspector",
"javascript",
]);
const PROVIDER_ROOTS = new Set([
...MIGRATED_PROVIDER_ROOTS,
"artifacts",
"browser",
"dotnet",
"native",
]);
const repositoryPath = (root, path) =>
relative(root, path).replaceAll("\\", "/");
@@ -88,11 +80,6 @@ const failedBoundary = (file, target) => {
/^src\/(?:cli|main)\./u.test(target))
)
return "artifact-acquisition";
if (
PROVIDER_ROOTS.has(layer) &&
/^src\/generatedMcpToolCatalog\.(?:js|ts)$/u.test(target)
)
return "provider-generated-catalog";
if (PURE_LAYERS.has(layer)) {
if (
(/^src\/[^/]+\//u.test(target) && !PURE_LAYERS.has(targetLayer)) ||
+8 -12
View File
@@ -179,12 +179,12 @@ async function verifyNativeValueAdapters(
{ timeout: 180000 },
);
assert.notEqual(result.isError, true, JSON.stringify(result));
const mcpEvidence = parseEvidence(result.structuredContent?.evidence);
const mcpEvidence = parseEvidence(result.structuredContent);
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,
result.structuredContent?.normalized_result,
target,
procedure,
globalAddress,
@@ -196,9 +196,7 @@ async function verifyNativeValueAdapters(
{ timeout: 180000 },
);
assert.notEqual(analyzed.isError, true, JSON.stringify(analyzed));
const functionEvidence = parseEvidence(
analyzed.structuredContent?.evidence,
);
const functionEvidence = parseEvidence(analyzed.structuredContent);
assert.equal(functionEvidence.operation, "analyze_function");
assert.equal(functionEvidence.provider.id, "ghidra");
const functionProfile = analysisProfileSchema.parse(
@@ -207,26 +205,24 @@ async function verifyNativeValueAdapters(
assert.deepEqual(functionProfile, upstreamProfile);
assert.deepEqual(
functionEvidence.normalized_result,
analyzed.structuredContent?.result,
analyzed.structuredContent?.normalized_result,
);
const comparison = await client.callTool(
{
name: "compare_functions",
arguments: {
left: analyzed.structuredContent.evidence,
right: analyzed.structuredContent.evidence,
left: analyzed.structuredContent,
right: analyzed.structuredContent,
},
},
{ timeout: 180000 },
);
assert.notEqual(comparison.isError, true, JSON.stringify(comparison));
const comparisonEvidence = parseEvidence(
comparison.structuredContent?.evidence,
);
const comparisonEvidence = parseEvidence(comparison.structuredContent);
assert.equal(comparisonEvidence.normalized_result.status, "unchanged");
assert.deepEqual(
comparisonEvidence.normalized_result,
comparison.structuredContent?.result,
comparison.structuredContent?.normalized_result,
);
assert.ok(
comparisonEvidence.evidence_links.includes(functionEvidence.evidence_id),
@@ -8,7 +8,7 @@ export async function verifyGhidraAddressBoundaries(client, entry, cli) {
{ timeout: 240000 },
);
assert.notEqual(reply.isError, true, JSON.stringify(reply));
return reply.structuredContent.result;
return reply.structuredContent.normalized_result;
};
const before = await query("analyze_function", { procedure: entry });
// The segmented parser used to discard the high 32 bits and alias this entry.
@@ -0,0 +1,41 @@
import assert from "node:assert/strict";
/** Verify batch selector/entry separation against real source-owned Ghidra functions. */
export async function verifyGhidraBatchIdentity(call, names) {
const leaf = names.find((item) =>
item.value.endsWith("rea_ghidra_inventory_leaf"),
);
assert.ok(leaf, "Source fixture lacks a leaf procedure");
const listing = await call("read_function_instructions", {
procedure: leaf.address,
});
const interior = listing.instructions[1]?.split(": ")[0];
assert.ok(interior, "Source fixture lacks an interior instruction");
const name = await call("address_name", { address: leaf.address });
const selectors = [leaf.address, interior, leaf.value, "0x0"];
const result = await call("batch_decompile", { addresses: selectors });
assert.equal(result.total, 4);
assert.equal(result.succeeded, 3);
assert.equal(result.failed, 1);
for (let index = 0; index < 3; index++) {
const item = result.items[index];
assert.equal(item.address, selectors[index]);
assert.equal(item.status, "ok");
assert.deepEqual(item.procedure, {
status: "resolved",
address: leaf.address,
name,
});
assert.equal(item.pseudocode, result.items[0].pseudocode);
}
assert.equal(result.items[3].status, "error");
assert.equal(result.items[3].procedure.status, "unknown");
assert.ok(result.items[3].procedure.error.code);
return {
entry: leaf.address,
interior,
selectors_preserved: true,
identical_resolved_code: true,
partial_failure_retained: true,
};
}
+39 -3
View File
@@ -8,12 +8,18 @@ import { promisify } from "node:util";
import Ajv from "ajv/dist/2020.js";
import addFormats from "ajv-formats";
import { analysisErrorProjectionSchema } from "../../dist/contracts/errorSchemas.js";
import { mcpTextValue, requireMcpResult } from "./mcp-verifier-results.mjs";
import {
mcpTextValue,
requireMcpOperationResult,
} from "./mcp-verifier-results.mjs";
import { verifyLegacyGhidraReferenceSnapshot } from "./ghidra-reference-snapshot-e2e.mjs";
import { verifyGhidraSnapshotLifecycle } from "./real-ghidra-snapshot-lifecycle.mjs";
import { verifyGhidraTargetAdmission } from "./real-ghidra-target-admission.mjs";
import { verifyGhidraLargeResults } from "./real-ghidra-large-results.mjs";
import { verifyGhidraEntryAliases } from "./real-ghidra-entry-aliases.mjs";
import { verifyGhidraNamespaceAnnotations } from "./real-ghidra-namespace-annotations.mjs";
import { verifyGhidraBatchIdentity } from "./real-ghidra-batch-identity.mjs";
import { verifyGhidraInstructionFlow } from "./real-ghidra-instruction-flow.mjs";
/** Probe real Ghidra location, annotation and error contracts through public adapters. */
export async function verifyGhidraBoundaries(
@@ -37,7 +43,7 @@ export async function verifyGhidraBoundaries(
let rejectedCalls = 0;
const call = async (name, args = {}) => {
const reply = await client.callTool({ name, arguments: args }, options);
const result = requireMcpResult(reply, name);
const result = requireMcpOperationResult(reply, name);
const validate = validators.get(name);
assert.ok(validate, `${name} missing from catalog`);
assert.ok(
@@ -156,6 +162,8 @@ export async function verifyGhidraBoundaries(
/Annotations require a local function entry/u,
);
const names = await call("list_names");
const batchIdentity = await verifyGhidraBatchIdentity(call, names);
const instructionFlow = await verifyGhidraInstructionFlow(call, names, cli);
const leaf = names.find((item) =>
item.value.endsWith("rea_ghidra_inventory_leaf"),
);
@@ -278,7 +286,25 @@ export async function verifyGhidraBoundaries(
await cli("inspect-native-instruction", address.toUpperCase()),
baseline,
);
const interior = `0x${(BigInt(address) + 1n).toString(16)}`;
// x86 function entries can begin with a one-byte PUSH. Locate an observed
// multi-byte instruction instead of assuming entry + 1 is an interior byte.
let multiByteInstruction;
for (const range of original.procedure.body.ranges) {
let cursor = BigInt(range.start);
while (cursor <= BigInt(range.end)) {
const instruction = await call("inspect_native_instruction", {
address: `0x${cursor.toString(16)}`,
});
if (instruction.status === "decoded" && instruction.length > 1) {
multiByteInstruction = instruction;
break;
}
cursor += BigInt(instruction.length ?? 1);
}
if (multiByteInstruction !== undefined) break;
}
assert.ok(multiByteInstruction, "Fixture lacks a multi-byte instruction");
const interior = `0x${(BigInt(multiByteInstruction.address) + 1n).toString(16)}`;
assert.equal(
(await call("inspect_native_instruction", { address: interior })).status,
"not-instruction-boundary",
@@ -693,6 +719,13 @@ export async function verifyGhidraBoundaries(
entrypoint,
env,
});
await verifyGhidraEntryAliases({
call,
reject: invalid,
target,
entrypoint,
env,
});
await verifyGhidraLargeResults({
call,
reject: invalid,
@@ -740,7 +773,10 @@ export async function verifyGhidraBoundaries(
missing_and_nonregular_source_rejected: true,
imported_source_identity_retained: true,
equivalent_instruction_address_spellings: true,
batch_procedure_identity: batchIdentity,
instruction_flow: instructionFlow,
qualified_annotation_name_roundtrip: true,
imported_entry_alias_selection: true,
oversized_result_retention_and_complete_export: true,
long_selector_rejection_and_provider_recovery: true,
source_immutable: true,
+190
View File
@@ -0,0 +1,190 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
/** Verify imported entry aliases through CLI/MCP without admitting interior labels. */
export async function verifyGhidraEntryAliases({
call,
reject,
target,
entrypoint,
env,
}) {
const exec = promisify(execFile);
const compiler = process.env.REA_CC ?? "cc";
const workspace = await mkdtemp(join(tmpdir(), "rea-ghidra-entry-aliases-"));
const source = fileURLToPath(
new URL("../../tests/conformance/ghidra/entry-aliases.c", import.meta.url),
);
const path = join(workspace, "entry-aliases");
const prefix = process.platform === "darwin" ? "_" : "";
try {
await exec(compiler, ["-O0", "-g", "-fno-inline", source, "-o", path], {
env,
timeout: 30000,
});
const bytes = await readFile(path);
await call("close_binary");
await call("open_binary", { path, provider_id: "ghidra" });
const names = await call("list_names");
const alias = names.find(
({ value }) => value === `${prefix}rea_entry_alias`,
);
assert.ok(alias, "Fixture entry alias was not imported");
assert.equal(
alias.symbol.primary,
false,
"The regression must exercise a secondary entry symbol",
);
assert.equal(alias.symbol.source, "imported");
const canonical = (await call("list_procedures")).find(
({ address }) => address === alias.address,
);
assert.ok(canonical, "Fixture alias is not at a function entry");
const xrefsSelectors = [];
for (const value of [
canonical.value,
alias.value,
`${prefix}rea_xrefs_data`,
`${prefix}rea_xrefs_unreferenced`,
]) {
const symbol = names.find((item) => item.value === value);
assert.ok(symbol, `Missing xrefs selector ${value}`);
const expected = await call("xrefs", { address: symbol.address });
if (value === `${prefix}rea_xrefs_unreferenced`)
assert.deepEqual(expected, []);
else assert.ok(expected.length > 0, `Missing references for ${value}`);
xrefsSelectors.push({ symbol, expected });
}
for (const procedure of [
alias.value,
"dead",
canonical.value,
alias.address,
]) {
assert.equal(
await call("procedure_address", { procedure }),
alias.address,
);
assert.equal(
(await call("analyze_function", { procedure })).procedure.address,
alias.address,
);
assert.equal(
await call("procedure_pseudo_code", { procedure }),
await call("procedure_pseudo_code", { procedure: alias.address }),
);
}
// Leaf and qualified forms of the primary name must not duplicate a match.
assert.equal(
(await call("procedure_info", { procedure: alias.value })).entrypoint,
alias.address,
);
const interior = names.find(
({ value }) => value === `${prefix}rea_interior`,
);
assert.ok(interior);
assert.notEqual(interior.address, alias.address);
const failure = await reject("procedure_address", {
procedure: interior.value,
});
assert.equal(failure.code, "invalid_request");
assert.match(
JSON.stringify(failure.details.issues),
/Unknown Ghidra procedure name or address/u,
);
assert.equal(
await call("procedure_address", { procedure: interior.address }),
alias.address,
"Explicit interior addresses keep their existing containment semantics",
);
const edited = await call("annotate_native_function", {
procedure: alias.value,
comment: "Selected by imported entry alias",
});
assert.equal(edited.annotations.address, alias.address);
assert.equal(
edited.annotations.comment,
"Selected by imported entry alias",
);
assert.equal(
await call("procedure_address", { procedure: alias.value }),
alias.address,
);
await call("close_binary");
const { stdout } = await exec(
process.execPath,
[
entrypoint,
"function",
path,
alias.value,
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
);
assert.equal(
JSON.parse(stdout).normalized_result.procedure.address,
alias.address,
);
for (const { symbol, expected } of xrefsSelectors) {
for (const selector of [
[symbol.value],
["--address", symbol.value],
[symbol.address],
]) {
const { stdout } = await exec(
process.execPath,
[
entrypoint,
"xrefs",
path,
...selector,
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
);
const evidence = JSON.parse(stdout);
assert.deepEqual(evidence.normalized_result, expected);
assert.equal(evidence.parameters.address, selector.at(-1));
}
}
await assert.rejects(
exec(
process.execPath,
[
entrypoint,
"xrefs",
path,
"rea_missing_symbol",
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
),
(error) => {
const failure = JSON.parse(error.stdout);
assert.equal(failure.code, "invalid_request");
assert.equal(failure.details.operation, "xrefs");
assert.match(
JSON.stringify(failure.details.issues),
/Unknown analyzed symbol name.*rea_missing_symbol/u,
);
return true;
},
);
assert.deepEqual(await readFile(path), bytes);
await call("open_binary", { path: target.path, provider_id: "ghidra" });
} finally {
await rm(workspace, { recursive: true, force: true });
}
}
@@ -0,0 +1,110 @@
import assert from "node:assert/strict";
/** Verify decoded return evidence and distinct non-return flows on owned fixtures. */
export async function verifyGhidraInstructionFlow(call, names, cli) {
const leaf = names.find((item) =>
item.value.endsWith("rea_ghidra_inventory_leaf"),
);
assert.ok(leaf, "Source fixture lacks its returning leaf");
const leafInstructions = await inspectFunction(call, leaf.address);
const returned = leafInstructions.find(
(instruction) => instruction.flow.kind === "return",
);
assert.ok(returned, "Returning source leaf lacks decoded RETURN evidence");
assertReturnEvidence(returned);
assert.equal(
(await call("resolve_native_call_targets", { address: returned.address }))
.status,
"not-call",
);
assert.deepEqual(
await cli("inspect-native-instruction", returned.address),
returned,
);
const controls = [
["rea_ghidra_flow_return", "RET", "c3"],
["rea_ghidra_flow_cleanup_return", "RET", "c21000"],
["rea_ghidra_flow_shared_tail", "JMP", null],
["rea_ghidra_flow_trap", "UD2", "0f0b"],
];
let checked = 0;
for (const [symbol, mnemonic, bytes] of controls) {
const procedure = names.find((item) => item.value === symbol);
if (procedure === undefined) continue;
const before = await call("resolve_containing_procedure", {
address: procedure.address,
});
const instructions = await inspectFunction(call, procedure.address);
const instruction = instructions.find((item) => item.mnemonic === mnemonic);
assert.ok(instruction, `Source fixture lacks ${symbol}'s ${mnemonic}`);
if (bytes !== null) assert.equal(instruction.bytes, bytes);
if (mnemonic === "RET") {
assertReturnEvidence(instruction);
assert.equal(instruction.flow.kind, "return");
if (bytes === "c21000")
assert.equal(instruction.operands[0]?.raw, "0x10");
} else {
assert.notEqual(instruction.flow.kind, "return");
assert.ok(
instruction.flow.classification_evidence.some(
(item) => item.source === "ghidra-listing-flow-type",
),
);
}
assert.deepEqual(
await call("resolve_containing_procedure", {
address: procedure.address,
}),
before,
);
checked++;
}
const unavailable = await call("inspect_native_instruction", {
address: "0x0",
});
assert.equal(unavailable.status, "outside-memory");
assert.equal(unavailable.flow.kind, "unavailable");
assert.deepEqual(unavailable.flow.classification_evidence, []);
return {
return_instruction: returned.address,
decoded_pcode_return: true,
return_cli_mcp_parity: true,
explicit_x86_elf_controls: checked,
explicit_control_scope:
checked === 0
? "Not present on this host-native fixture format/architecture"
: "x86-64 ELF",
generic_unknown_preserved: true,
};
}
async function inspectFunction(call, procedure) {
const listing = await call("read_function_instructions", { procedure });
const instructions = [];
for (const line of listing.instructions) {
const delimiter = line.indexOf(": ");
assert.ok(delimiter > 0, `Malformed provider instruction listing: ${line}`);
instructions.push(
await call("inspect_native_instruction", {
address: line.slice(0, delimiter),
}),
);
}
return instructions;
}
function assertReturnEvidence(instruction) {
assert.equal(instruction.status, "decoded");
assert.ok(
instruction.flow.classification_evidence.some(
(item) =>
item.source === "ghidra-instruction-pcode" && item.value === "RETURN",
),
);
assert.ok(
!instruction.limitations.some((text) =>
text.includes("not classified as a return"),
),
);
}
@@ -161,6 +161,7 @@ export async function verifyGhidraNamespaceAnnotations({
async function requireNamespaceDemangler(env) {
const inspection = inspectGhidraInstallation({
environment: process.env,
installDir: env.GHIDRA_INSTALL_DIR,
...(env.JAVA_HOME === undefined ? {} : { javaHome: env.JAVA_HOME }),
});
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { parseAnalysisSnapshot } from "../../dist/domain/analysisSnapshot.js";
import { parseEvidenceBundle } from "../../dist/domain/evidenceBundle.js";
import { requireMcpResult } from "./mcp-verifier-results.mjs";
import { requireMcpOperationResult } from "./mcp-verifier-results.mjs";
/** Keep mutable Ghidra observations out of immutable snapshots across target opens. */
export async function verifyGhidraSnapshotLifecycle(
@@ -17,7 +17,7 @@ export async function verifyGhidraSnapshotLifecycle(
let successfulCalls = 0;
let rejectedCalls = 0;
const call = async (name, args = {}) => {
const result = requireMcpResult(
const result = requireMcpOperationResult(
await client.callTool({ name, arguments: args }, { timeout: 180000 }),
name,
);
+24 -7
View File
@@ -6,8 +6,14 @@ import { basename, join } from "node:path";
import { promisify } from "node:util";
import Ajv from "ajv/dist/2020.js";
import addFormats from "ajv-formats";
import { mcpTextValue, requireMcpResult } from "./mcp-verifier-results.mjs";
import {
mcpTextValue,
requireMcpEvidenceResult,
requireMcpOperationResult,
requireMcpLifecycleResult,
} from "./mcp-verifier-results.mjs";
import { verifyHopperCliSelectors } from "./real-hopper-cli-selectors.mjs";
import { verifyLinuxHopperLaunchIsolation } from "./real-hopper-session-isolation.mjs";
import { verifyHopperNavigationAndText } from "./real-hopper-navigation.mjs";
import { verifyHopperWorkflows } from "./real-hopper-workflows.mjs";
import {
@@ -43,7 +49,7 @@ export async function verifyHopperBoundaryContracts(
const rejectedOperations = new Set();
const call = async (name, args = {}) => {
const reply = await client.callTool({ name, arguments: args }, options);
const result = requireMcpResult(reply, name);
const result = requireMcpOperationResult(reply, name);
const validate = validators.get(name);
assert.ok(validate, `${name} is missing from tools/list`);
assert.ok(
@@ -300,7 +306,7 @@ export async function verifyHopperLifecycleAndCli(client, options, targets) {
const primary = join(directory, `primary-${suffix}`);
const secondary = join(directory, `secondary-${suffix}`);
const call = async (name, args = {}) =>
requireMcpResult(
requireMcpOperationResult(
await client.callTool({ name, arguments: args }, options),
name,
);
@@ -327,6 +333,16 @@ export async function verifyHopperLifecycleAndCli(client, options, targets) {
procedure: entry.value,
});
const firstDocument = await call("current_document");
const linuxApplicationIsolation =
process.platform === "linux"
? await verifyLinuxHopperLaunchIsolation({
call,
runCli,
dispatcher: new URL("../rea.mjs", import.meta.url).pathname,
primary,
secondary,
})
: null;
await call("open_binary", { path: secondary });
const secondDocument = await call("current_document");
assert.notEqual(firstDocument, secondDocument);
@@ -464,6 +480,7 @@ export async function verifyHopperLifecycleAndCli(client, options, targets) {
cliNamedSelectorParity,
callerCancellationRecovered: true,
closedDocumentAbsent: true,
linuxApplicationIsolation,
};
} finally {
await call("close_binary");
@@ -496,14 +513,14 @@ const verifyBatchCancellation = async (client, options, procedures) => {
/aborted/u,
);
assert.ok(cancelled, "cancellation did not follow a real Hopper response");
const recovered = requireMcpResult(
const recovered = requireMcpEvidenceResult(
await client.callTool({ name: "list_procedures", arguments: {} }, options),
"list_procedures after cancellation",
"list_procedures",
);
assert.deepEqual(recovered, procedures);
const status = requireMcpResult(
const status = requireMcpLifecycleResult(
await client.callTool({ name: "binary_session", arguments: {} }, options),
"binary_session after cancellation",
"binary_session",
);
assert.equal(status.provider_operation_health.state, "idle");
};
+4 -4
View File
@@ -1,12 +1,12 @@
import assert from "node:assert/strict";
import { requireMcpResult } from "./mcp-verifier-results.mjs";
import { requireMcpEvidenceResult } from "./mcp-verifier-results.mjs";
export const verifyHopperFunctionBasics = async (
client,
options,
procedure,
) => {
const containment = requireMcpResult(
const containment = requireMcpEvidenceResult(
await client.callTool(
{
name: "resolve_containing_procedure",
@@ -25,7 +25,7 @@ export const verifyHopperFunctionBasics = async (
);
}
const references = requireMcpResult(
const references = requireMcpEvidenceResult(
await client.callTool(
{
name: "procedure_references",
@@ -42,7 +42,7 @@ export const verifyHopperFunctionBasics = async (
throw new Error("procedure_references returned an invalid result");
}
const instructions = requireMcpResult(
const instructions = requireMcpEvidenceResult(
await client.callTool(
{
name: "read_function_instructions",
+3 -3
View File
@@ -1,7 +1,7 @@
import assert from "node:assert/strict";
import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProvider.js";
import {
requireMcpResult,
requireMcpEvidenceResult,
requireEvidenceProvider,
} from "./mcp-verifier-results.mjs";
@@ -13,7 +13,7 @@ export async function verifyHopperInventories(client, options) {
{ name: operation, arguments: {} },
options,
);
const inventory = requireMcpResult(called, operation);
const inventory = requireMcpEvidenceResult(called, operation);
requireEvidenceProvider(called, operation, HOPPER_PROVIDER_IDENTITY.id);
assert.ok(
Array.isArray(inventory) && inventory.length > 0,
@@ -30,7 +30,7 @@ export async function verifyHopperInventories(client, options) {
);
previous = address;
}
const filtered = requireMcpResult(
const filtered = requireMcpEvidenceResult(
await client.callTool(
{
name: operation,
+4 -4
View File
@@ -107,7 +107,7 @@ export async function verifyRealHopperFixture({
},
options,
),
"set_comment fixture marker",
"set_comment",
);
if (commentSet !== true)
throw new Error("Hopper did not read back the fixture comment marker");
@@ -138,7 +138,7 @@ export async function verifyRealHopperFixture({
},
options,
),
"procedure_references native calls",
"procedure_references",
);
assert.deepEqual(entryDossier.unresolved_calls, observed.unresolved_calls);
for (const call of calls) {
@@ -266,7 +266,7 @@ export async function resolveFixtureProcedure(
},
options,
),
`search_procedures ${expectedName}`,
"search_procedures",
);
const exactMatches = Array.isArray(matches)
? matches.filter(
@@ -301,5 +301,5 @@ export const analyzeFixtureProcedure = async (
},
options,
),
`analyze_function ${procedure.name}`,
"analyze_function",
);
@@ -0,0 +1,53 @@
import assert from "node:assert/strict";
/** Prove competing Linux launches cannot forward documents into the active MCP owner. */
export async function verifyLinuxHopperLaunchIsolation({
call,
runCli,
dispatcher,
primary,
secondary,
}) {
const documents = await call("list_documents");
const current = await call("current_document");
const procedures = await call("list_procedures");
for (const target of [primary, secondary]) {
let failure;
try {
await runCli([
dispatcher,
"inspect",
target,
"--provider",
"hopper",
"--format",
"json",
]);
} catch (error) {
assert.equal(error.code, 1);
failure = JSON.parse(error.stdout);
}
assert.ok(
failure,
"A competing CLI launch was allowed to reach Linux Hopper",
);
assert.equal(failure.code, "provider_unavailable");
assert.match(
failure.message,
/Linux Hopper is already active in REA session/u,
);
assert.ok(failure.message.includes(target));
assert.deepEqual(
await call("list_documents"),
documents,
"A rejected CLI launch changed the owning session's documents",
);
assert.equal(await call("current_document"), current);
assert.deepEqual(await call("list_procedures"), procedures);
}
return {
sameTargetRejected: true,
differentTargetRejected: true,
ownerDocumentsPreserved: true,
};
}
+237
View File
@@ -0,0 +1,237 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { setTimeout as delay } from "node:timers/promises";
/** Exercise owned Windows stdin through its production Writable and native boundary. */
export async function verifyWindowsProtocolInput(packageRoot, workspace) {
const { WindowsOwnedProcess } = await import(
pathToFileURL(join(packageRoot, "dist/windows/WindowsOwnedProcess.js"))
);
const { requireWindowsNativeAuthority } = await import(
pathToFileURL(join(packageRoot, "dist/windows/WindowsNativeLoader.js"))
);
const native = requireWindowsNativeAuthority();
assert.equal(native.inspection.protocolStdin, true);
const launch = (script) => acquire(WindowsOwnedProcess, workspace, script);
const echo = await verifyEcho(launch);
const cancellation = await verifyBlockedCancellation(launch);
await verifyNativePendingWrite(native, workspace);
const { JadxMcpTransport } = await import(
pathToFileURL(join(packageRoot, "dist/android/JadxMcpTransport.js"))
);
await verifyTransportPendingWrite(JadxMcpTransport, workspace);
return {
echo,
cancellation,
native_pending_write_closed: true,
forged_input_handle_rejected: true,
malformed_input_rejected: true,
provider_transport_pending_write_cancelled: true,
};
}
function acquire(Process, workspace, script) {
const child = new Process(
process.execPath,
["-e", script],
workspace,
process.env,
false,
true,
);
let stdout = "";
let stderr = "";
child.stdout.on("data", (bytes) => {
stdout += bytes.toString();
});
child.stderr.on("data", (bytes) => {
stderr += bytes.toString();
});
const inputErrors = [];
child.stdin.on("error", (cause) => inputErrors.push(cause.message));
const closed = new Promise((resolve, reject) => {
child.once("error", reject);
child.once("close", (code) => resolve({ code, stdout, stderr }));
});
return { child, closed, inputErrors, stdout: () => stdout };
}
async function verifyEcho(launch) {
const acquired = launch(`
const {createHash} = require('node:crypto');
const hash = createHash('sha256'); let count = 0;
process.stdin.on('data', bytes => { hash.update(bytes); count += bytes.length; });
process.stdin.pause(); setTimeout(() => process.stdin.resume(), 250);
process.stdin.on('end', () => console.log(JSON.stringify({count,sha256:hash.digest('hex')})));
`);
const bytes = Buffer.from(
"caller-selected UTF8: 路径 Ω; binary:\0\n".repeat(60000),
);
const expected = {
count: bytes.length,
sha256: createHash("sha256").update(bytes).digest("hex"),
};
let ticks = 0;
const timer = setInterval(() => ticks++, 20);
try {
let writable;
await new Promise((resolve, reject) => {
writable = acquired.child.stdin.write(bytes, (cause) =>
cause ? reject(cause) : resolve(),
);
// The native worker owns its copy before returning to the caller.
bytes.fill(0x5a);
});
assert.equal(
writable,
false,
"Large protocol input did not expose backpressure",
);
assert.ok(ticks > 0, "Input write blocked the JS event loop");
acquired.child.stdin.end();
const result = await deadline(acquired.closed);
assert.equal(result.code, 0, result.stderr);
assert.deepEqual(JSON.parse(result.stdout), expected);
assert.deepEqual(acquired.inputErrors, []);
assert.equal((await acquired.child.cleanup()).cleaned, true);
return {
bytes: expected.count,
sha256: expected.sha256,
backpressure: true,
event_loop_responsive: true,
eof_observed: true,
};
} finally {
clearInterval(timer);
await acquired.child.cleanup();
}
}
async function verifyBlockedCancellation(launch) {
const acquired = launch(`
const {spawn} = require('node:child_process');
const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], {stdio:'inherit'});
console.log('owned descendant ' + child.pid);
setInterval(() => {}, 1000);
`);
try {
for (
let attempt = 0;
attempt < 100 && !acquired.stdout().includes("owned descendant");
attempt++
)
await delay(25);
assert.match(acquired.stdout(), /owned descendant \d+/u);
const write = new Promise((resolve) =>
acquired.child.stdin.write(Buffer.alloc(2 * 1024 * 1024), (cause) =>
resolve(cause),
),
);
await delay(100);
const cleanup = await acquired.child.cleanup();
assert.equal(cleanup.cleaned, true, cleanup.reason);
assert.ok(
await deadline(write),
"Closing the owned job reported a successful blocked write",
);
await deadline(acquired.closed);
return { blocked_write_cancelled: true, descendant_job_settled: true };
} finally {
await acquired.child.cleanup();
}
}
async function verifyNativePendingWrite(native, workspace) {
const quote = (value) =>
`"${value.replace(/(\\*)"/gu, '$1$1\\"').replace(/(\\+)$/u, "$1$1")}"`;
const child = native.call("process_spawn", [
process.execPath,
[process.execPath, "-e", "setInterval(() => {}, 1000)"]
.map(quote)
.join(" "),
workspace,
Object.entries(process.env)
.filter(([, value]) => value !== undefined)
.map(([key, value]) => `${key}=${value}`),
true,
]);
let rejected;
try {
assert.throws(
() => native.call("process_stdin_write", [child.handle, "not a Buffer"]),
/must be a Buffer/u,
);
const pending = native.call("process_stdin_write", [
child.handle,
Buffer.alloc(2 * 1024 * 1024),
]);
rejected = pending.then(
() => false,
(cause) => cause,
);
assert.throws(
() =>
native.call("process_stdin_write", [
child.handle,
Buffer.from("second"),
]),
/pending write/u,
);
assert.throws(
() => native.call("process_stdin_write", [{}, Buffer.from("forged")]),
/does not belong to REA/u,
);
} finally {
// The worker retains its handle/buffer after the JS resource closes.
native.call("process_close", [child.handle]);
}
const failure = await deadline(rejected);
assert.ok(failure instanceof Error);
assert.match(failure.message, /Write owned process input failed/u);
}
async function verifyTransportPendingWrite(Transport, workspace) {
const transport = new Transport({
command: process.execPath,
arguments: ["-e", "setInterval(() => {}, 1000)"],
cwd: workspace,
});
transport.onerror = () => {};
try {
await transport.start();
const pending = transport
.send({
jsonrpc: "2.0",
id: "blocked",
method: "probe",
params: { payload: "x".repeat(2 * 1024 * 1024) },
})
.then(
() => false,
(cause) => cause,
);
await delay(100);
await transport.close();
assert.ok(
await deadline(pending),
"Transport cancellation accepted a blocked write",
);
} finally {
await transport.close();
}
}
async function deadline(promise) {
let timer;
return Promise.race([
promise,
new Promise((_, reject) => {
timer = setTimeout(
() => reject(new Error("Owned input fixture exceeded its deadline")),
10000,
);
}),
]).finally(() => clearTimeout(timer));
}
+3 -6
View File
@@ -44,7 +44,6 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
const {
renderCliOutputArgumentError,
renderEmptyFilteredCliOutput,
sanitizeCliOutput,
validateCliOutputArguments,
} = await import("../dist/cliOutput.js");
const outputArguments = validateCliOutputArguments(args);
@@ -57,13 +56,11 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
await createCli(process.env, resultOutput).serve(args, {
stdout: (output) => {
if (resultOutput?.handled) {
if (resultOutput.failed)
process.stderr.write(sanitizeCliOutput(output));
if (resultOutput.failed) process.stderr.write(output);
return;
}
const sanitized = sanitizeCliOutput(output);
if (sanitized.length > 0) wroteOutput = true;
process.stdout.write(sanitized);
if (output.length > 0) wroteOutput = true;
process.stdout.write(output);
},
});
if (!wroteOutput && !resultOutput?.handled)
+17 -8
View File
@@ -47,15 +47,24 @@ try {
? `Focused tests: ${request.paths.join(", ")}`
: `Source-test feedback${baseCommit ? ` from merge base ${baseCommit}` : ""}; compiled boundaries and real providers need explicit verification.`,
);
if (plan.needsBuild) {
if (!process.env.npm_execpath)
throw new Error("Run compiled tests through npm run test:focused");
const built = await run(process.execPath, [
process.env.npm_execpath,
"run",
"build:cached",
if (plan.needsBuild || plan.artifactTasks.length > 0) {
const dependencies = await run(process.execPath, [
"scripts/check-dependency-install.mjs",
]);
if (built.code !== 0) process.exitCode = built.code;
if (dependencies.code !== 0)
throw new Error(
"Test prerequisites require the locked dependency installation",
);
const prepared = await run(process.execPath, [
"scripts/run-exclusive.mjs",
"artifacts",
process.execPath,
resolve("node_modules/turbo/bin/turbo"),
"run",
...(plan.needsBuild ? ["artifacts:skills"] : []),
...plan.artifactTasks,
]);
if (prepared.code !== 0) process.exitCode = prepared.code;
}
if (!process.exitCode && !shutdownSignal) {
const tested = await run(process.execPath, [
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env node
import { spawn } from "node:child_process";
import { mkdir } from "node:fs/promises";
import { join, resolve } from "node:path";
import { DatabaseSync } from "node:sqlite";
const [, , lockName, ...command] = process.argv;
const lockRoot = resolve(process.cwd(), ".cache", "rea-command-locks");
const signalExitCodes = { SIGINT: 130, SIGTERM: 143 };
let database;
let child;
let forceTimer;
let shutdownSignal;
process.on("SIGINT", () => handleSignal("SIGINT"));
process.on("SIGTERM", () => handleSignal("SIGTERM"));
process.on("disconnect", handleDisconnect);
process.on("message", (message) => {
if (message === "SIGINT" || message === "SIGTERM") handleSignal(message);
});
if (!process.connected) {
// The public wrapper can disappear before ESM startup installs listeners.
// A disconnected supervisor has no authority to start a new command.
process.exitCode = signalExitCodes.SIGTERM;
} else if (
typeof lockName !== "string" ||
!/^[a-z][a-z0-9-]*$/u.test(lockName) ||
command.length === 0
) {
console.error("Usage: run-exclusive.mjs <lock-name> <command> [args...]");
process.exitCode = 64;
disconnectParent();
} else {
await run();
}
async function run() {
try {
await mkdir(lockRoot, { recursive: true });
// SQLite owns acquisition and OS-backed lock release, including process
// death. Keep the database pathname stable; deleting it would split owners.
database = new DatabaseSync(join(lockRoot, `${lockName}.sqlite`));
try {
database.exec("BEGIN EXCLUSIVE");
} catch (cause) {
if (isBusyError(cause))
throw new Error(
`Another ${lockName} command is already running. Wait for it to finish.`,
{ cause },
);
throw cause;
}
if (shutdownSignal === undefined) {
const result = await runCommand();
process.exitCode = result.code ?? 1;
}
} catch (cause) {
console.error(cause instanceof Error ? cause.message : String(cause));
process.exitCode = 1;
} finally {
database?.close();
clearTimeout(forceTimer);
if (shutdownSignal !== undefined)
process.exitCode = signalExitCodes[shutdownSignal];
disconnectParent();
}
}
function runCommand() {
return new Promise((resolveResult, reject) => {
child = spawn(command[0], command.slice(1), {
cwd: process.cwd(),
env: process.env,
shell: process.platform === "win32",
stdio: "inherit",
});
child.once("error", reject);
child.once("close", (code) => {
// During POSIX cancellation the group leader must stay alive, retaining
// both its group identity and SQLite ownership until the entire group is
// killed. A completed command can still have uncooperative descendants.
if (shutdownSignal === undefined || process.platform === "win32")
resolveResult({ code });
});
});
}
function disconnectParent() {
process.off("disconnect", handleDisconnect);
if (process.connected) process.disconnect();
}
function handleDisconnect() {
handleSignal("SIGTERM");
}
function handleSignal(signal) {
if (shutdownSignal !== undefined) return;
shutdownSignal = signal;
if (child === undefined || process.platform === "win32") return;
// Establish cancellation before signalling the group. This supervisor is
// its live leader, so the group identifier cannot be reused during cleanup.
process.kill(-process.pid, signal);
forceTimer = setTimeout(() => process.kill(-process.pid, "SIGKILL"), 1_000);
}
function isBusyError(cause) {
return (
typeof cause === "object" &&
cause !== null &&
"code" in cause &&
cause.code === "ERR_SQLITE_ERROR" &&
"errcode" in cause &&
(cause.errcode === 5 || cause.errcode === 6)
);
}
+31 -146
View File
@@ -1,155 +1,40 @@
#!/usr/bin/env node
import { spawn } from "node:child_process";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const [, , lockName, ...command] = process.argv;
const lockRoot = resolve(process.cwd(), ".cache", "rea-command-locks");
const lockPath = join(lockRoot, `${lockName ?? "invalid"}.lock`);
const ownerPath = join(lockPath, "owner.json");
const signalExitCodes = { SIGINT: 130, SIGTERM: 143 };
let lockHeld = false;
let child;
let forceTimer;
let shutdownSignal;
const supervisor = spawn(
process.execPath,
[
fileURLToPath(new URL("./run-exclusive-command.mjs", import.meta.url)),
...process.argv.slice(2),
],
{
detached: process.platform !== "win32",
stdio: ["inherit", "inherit", "inherit", "ipc"],
},
);
process.on("SIGINT", () => handleSignal("SIGINT"));
process.on("SIGTERM", () => handleSignal("SIGTERM"));
if (
typeof lockName !== "string" ||
!/^[a-z][a-z0-9-]*$/u.test(lockName) ||
command.length === 0
) {
console.error("Usage: run-exclusive.mjs <lock-name> <command> [args...]");
process.exitCode = 64;
} else {
await run();
}
async function run() {
try {
await acquireLock();
const result = await runCommand();
process.exitCode =
shutdownSignal === undefined
? (result.code ?? 1)
: signalExitCodes[shutdownSignal];
} catch (cause) {
console.error(cause instanceof Error ? cause.message : String(cause));
process.exitCode = 1;
} finally {
await releaseLock();
}
}
async function acquireLock() {
await mkdir(lockRoot, { recursive: true });
for (let attempt = 0; attempt < 3; attempt += 1) {
try {
await mkdir(lockPath);
await writeFile(
ownerPath,
JSON.stringify({
pid: process.pid,
command: command.join(" "),
started_at: new Date().toISOString(),
}),
"utf8",
);
lockHeld = true;
return;
} catch (cause) {
if (!isAlreadyExistsError(cause)) throw cause;
const owner = await readOwner();
if (owner === null || isProcessAlive(owner.pid))
throw new Error(
`Another ${lockName} command is already running (pid ${String(owner?.pid ?? "unknown")}). Wait for it to finish or inspect ${lockPath}.`,
);
await rm(lockPath, { force: true, recursive: true });
}
}
throw new Error(`Could not acquire the ${lockName} command lock`);
}
async function releaseLock() {
if (!lockHeld) return;
lockHeld = false;
try {
const owner = await readOwner();
if (owner?.pid === process.pid)
await rm(lockPath, { force: true, recursive: true });
} catch (cause) {
console.error(
`Could not remove the ${lockName} command lock: ${cause instanceof Error ? cause.message : String(cause)}`,
);
}
}
function runCommand() {
return new Promise((resolveResult, reject) => {
child = spawn(command[0], command.slice(1), {
cwd: process.cwd(),
env: process.env,
shell: process.platform === "win32",
stdio: "inherit",
});
child.once("error", reject);
child.once("close", (code) => {
clearTimeout(forceTimer);
resolveResult({ code });
});
for (const signal of Object.keys(signalExitCodes))
process.on(signal, () => {
if (shutdownSignal !== undefined) return;
shutdownSignal = signal;
if (supervisor.connected)
supervisor.send(signal, (cause) => {
if (cause !== null)
console.error(`Could not forward cancellation: ${cause.message}`);
});
});
}
function handleSignal(signal) {
if (shutdownSignal !== undefined) return;
shutdownSignal = signal;
if (child !== undefined && child.exitCode === null) {
child.kill(signal);
forceTimer = setTimeout(() => child?.kill("SIGKILL"), 1_000);
forceTimer.unref?.();
}
}
async function readOwner() {
try {
const parsed = JSON.parse(await readFile(ownerPath, "utf8"));
const pid =
typeof parsed === "object" && parsed !== null
? Reflect.get(parsed, "pid")
: undefined;
if (typeof pid !== "number" || !Number.isInteger(pid)) return null;
return { pid };
} catch (cause) {
if (isMissingFileError(cause)) return null;
throw cause;
}
}
function isProcessAlive(pid) {
try {
process.kill(pid, 0);
return true;
} catch (cause) {
return !isMissingProcessError(cause);
}
}
function isAlreadyExistsError(cause) {
return isNodeError(cause) && cause.code === "EEXIST";
}
function isMissingFileError(cause) {
return isNodeError(cause) && cause.code === "ENOENT";
}
function isMissingProcessError(cause) {
return isNodeError(cause) && cause.code === "ESRCH";
}
function isNodeError(cause) {
return typeof cause === "object" && cause !== null && "code" in cause;
}
supervisor.once("error", (cause) => {
console.error(cause.message);
process.exitCode = 1;
});
supervisor.once("close", (code) => {
process.exitCode =
shutdownSignal === undefined
? (code ?? 1)
: signalExitCodes[shutdownSignal];
});
+39 -74
View File
@@ -5,9 +5,16 @@ import { dirname, join, resolve } from "node:path";
import { createInterface } from "node:readline";
import { fileURLToPath } from "node:url";
import { createPackage } from "@electron/asar";
import { evaluateCodexEvents } from "../dist/evaluation/CodexAgentEval.js";
import {
createAgentEvaluationFixtures,
agentFixtureClaims,
agentFixtureAnswerInstructions,
} from "../dist/evaluation/AgentEvaluationFixtures.js";
import {
agentEvaluationPassed,
summarizeFactualCorrectness,
} from "../dist/evaluation/AgentEvaluationReport.js";
import { MCP_STARTUP_POLICY } from "../dist/mcpStartupPolicy.js";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
@@ -91,6 +98,10 @@ try {
expectedFirstTool: "inspect_managed_artifact",
requiresEvidence: true,
requiredAnswerTermGroups: [["profile"], ["main", "entry point"]],
requiredToolSubsequence: [
"inspect_managed_artifact",
"inspect_managed_members",
],
prompt: `Explain the public managed types and entry point in ${targets.managed}. Use shipped-artifact evidence and distinguish unavailable behavior from observed metadata.`,
},
{
@@ -134,7 +145,13 @@ try {
const results = [];
for (const scenario of scenarios) {
process.stderr.write(`Running Codex agent evaluation: ${scenario.id}\n`);
const execution = await runCodex(scenario.prompt);
const fixtureClaims = agentFixtureClaims(scenario.id, targets);
const answerInstructions = agentFixtureAnswerInstructions(scenario.id);
const execution = await runCodex(
answerInstructions.length === 0
? scenario.prompt
: `${scenario.prompt}\n\n${answerInstructions}`,
);
const transcriptDirectory = process.env.REA_AGENT_EVAL_TRANSCRIPT_DIR;
if (transcriptDirectory !== undefined) {
const transcriptPath = resolve(
@@ -155,14 +172,16 @@ try {
requiredAnswerTermGroups: scenario.requiredAnswerTermGroups,
requiredToolSubsequence: scenario.requiredToolSubsequence,
forbidInputValidationFailures: true,
...(fixtureClaims === undefined ? {} : { fixtureClaims }),
},
);
const result = {
id: scenario.id,
expectedFirstTool: scenario.expectedFirstTool,
requiresEvidence: scenario.requiresEvidence,
qualityCriteria: scenario.requiredAnswerTermGroups,
requiredAnswerTermGroups: scenario.requiredAnswerTermGroups,
requiredToolSubsequence: scenario.requiredToolSubsequence ?? [],
configuredClaimIds: fixtureClaims?.map(({ id }) => id) ?? [],
exitCode: execution.exitCode,
stderr: execution.stderr,
...metrics,
@@ -173,12 +192,16 @@ try {
);
}
const factualCorrectness = summarizeFactualCorrectness(results);
const summary = {
schemaVersion: 3,
evaluationScope: "routing_workflow_and_configured_fixture_claims",
factualCorrectness,
verifier_run: await completeVerifierRun(verifierRun),
codex,
codexVersion,
model: optionalModel ?? null,
scenarios: results.map(({ finalMessage: _message, ...result }) => result),
scenarios: results,
totals: {
scenarios: results.length,
naturalUse: results.filter(({ naturalUse }) => naturalUse).length,
@@ -205,12 +228,15 @@ try {
(total, { cachedInputTokens }) => total + cachedInputTokens,
0,
),
completionQuality: results.filter(({ completionQuality }) =>
Boolean(completionQuality),
answerHeuristicsMet: results.filter(({ answerHeuristicsMet }) =>
Boolean(answerHeuristicsMet),
).length,
authorityHonesty: results.filter(({ authorityHonesty }) =>
Boolean(authorityHonesty),
epistemicCuePresent: results.filter(({ epistemicCuePresent }) =>
Boolean(epistemicCuePresent),
).length,
factualScenariosPassed: factualCorrectness.counts.passed,
factualScenariosFailed: factualCorrectness.counts.failed,
factualScenariosNotAssessed: factualCorrectness.counts.notAssessed,
},
};
const encoded = `${JSON.stringify(summary, null, 2)}\n`;
@@ -221,31 +247,10 @@ try {
await writeFile(resolve(reportPath), encoded);
}
const failed = results.filter(
({
exitCode,
naturalUse,
correctFirstTool,
repeatedCallCount,
inputValidationFailureCount,
requiredToolSubsequenceMet,
inputTokens,
completionQuality,
authorityHonesty,
}) =>
exitCode !== 0 ||
!naturalUse ||
!correctFirstTool ||
repeatedCallCount !== 0 ||
inputValidationFailureCount !== 0 ||
!requiredToolSubsequenceMet ||
inputTokens <= 0 ||
!completionQuality ||
!authorityHonesty,
);
const failed = results.filter((result) => !agentEvaluationPassed(result));
if (failed.length > 0)
throw new Error(
`Codex agent release evaluation failed: ${failed.map(({ id }) => id).join(", ")}`,
`Agent routing/workflow/answer checks failed: ${failed.map(({ id }) => id).join(", ")}`,
);
} finally {
if (process.env.REA_AGENT_EVAL_KEEP_FIXTURES !== "true")
@@ -253,45 +258,7 @@ try {
}
async function createTargets(root, includeManaged) {
const javascript = join(root, "desktop-app");
await mkdir(join(javascript, "renderer"), { recursive: true });
await Promise.all([
writeFile(
join(javascript, "package.json"),
`${JSON.stringify({ name: "desktop-fixture", version: "1.0.0", main: "main.js" }, null, 2)}\n`,
),
writeFile(
join(javascript, "main.js"),
'const { BrowserWindow, ipcMain } = require("electron");\nnew BrowserWindow({ webPreferences: { preload: require("node:path").join(__dirname, "preload.js"), contextIsolation: true, sandbox: true } });\nipcMain.handle("profile:read", (_event, id) => ({ id }));\n',
),
writeFile(
join(javascript, "preload.js"),
'const { contextBridge, ipcRenderer } = require("electron");\ncontextBridge.exposeInMainWorld("profileApi", { read: (id) => ipcRenderer.invoke("profile:read", id) });\n',
),
writeFile(
join(javascript, "renderer/app.js"),
'globalThis.profileApi.read("fixture");\n',
),
]);
const javascriptAsar = join(root, "desktop-app.asar");
await createPackage(javascript, javascriptAsar);
const javascriptShapeLeft = join(root, "parser-v1");
const javascriptShapeRight = join(root, "parser-v2");
await Promise.all([
mkdir(javascriptShapeLeft, { recursive: true }),
mkdir(javascriptShapeRight, { recursive: true }),
]);
await Promise.all([
writeFile(
join(javascriptShapeLeft, "parser.mjs"),
'export default function parse() { return { heading: "Title" }; }\n',
),
writeFile(
join(javascriptShapeRight, "parser.mjs"),
'export default function parse() { return { heading: { text: "Title", level: 1 } }; }\n',
),
]);
const fixtures = await createAgentEvaluationFixtures(root, repositoryRoot);
const managedOutput = join(root, "managed-output");
if (includeManaged) {
@@ -324,9 +291,7 @@ async function createTargets(root, includeManaged) {
}
return {
native: "/bin/true",
javascript: javascriptAsar,
javascriptShapeLeft,
javascriptShapeRight,
...fixtures,
managed: join(managedOutput, "AgentEval.dll"),
};
}
+1 -2
View File
@@ -1254,8 +1254,7 @@ async function inspect(
return value.error;
}
assert.notEqual(response.isError, true, mcpTextValue(response));
envelope = value.evidence;
assert.deepEqual(value.result, envelope.normalized_result);
envelope = value;
} else {
let response;
try {
+2 -2
View File
@@ -118,7 +118,7 @@ try {
undefined,
`Unexpected public failure: ${JSON.stringify(value)}`,
);
const evidence = parseEvidence(mode === "cli" ? value : value.evidence);
const evidence = parseEvidence(value);
const report = evidence.normalized_result;
assert.equal(report.diagnostics.truncated, false);
assert.equal(evidence.provider.version, "evmole@0.9.3");
@@ -157,7 +157,7 @@ try {
})),
evidence.raw_result.functions ?? [],
);
if (mode === "mcp") assert.deepEqual(value.result, report);
if (mode === "mcp") assert.deepEqual(value.normalized_result, report);
return report;
};
const ownershipProbe = join(root.path, "ownership-prerequisite.hex");
+10 -3
View File
@@ -118,9 +118,14 @@ try {
);
const target = await parseBinaryTarget(executable);
if (!target.ok) throw target.error;
const observation = await observeNativeUi(target.value, "observe_native_ui", {
...scope,
});
const observation = await observeNativeUi(
target.value,
"observe_native_ui",
{
...scope,
},
{ environment: process.env },
);
if (
!observation.ok &&
!/accessibility-denied|screen-recording-denied/u.test(
@@ -150,6 +155,7 @@ try {
{ kind: "wait", milliseconds: 200 },
],
},
{ environment: process.env },
);
if (!scenario.ok) throw scenario.error;
if (
@@ -234,6 +240,7 @@ try {
{ ...target.value, sha256: "0".repeat(64) },
"observe_native_ui",
scope,
{ environment: process.env },
);
if (mismatch.ok || !mismatch.error.message.includes("target-mismatch"))
throw new Error("Real helper did not reject changed target bytes");
+9 -5
View File
@@ -8,13 +8,17 @@ export async function verifyPackageCapabilitiesAndSearch({ cli, environment }) {
if (!Array.isArray(capabilities.capabilities))
throw new Error("packaged capabilities CLI failed");
const providers = json(await run(cli, ["providers", "--json"], environment));
const candidates = providers.analysis_provider_candidates;
if (
!Array.isArray(providers.providers) ||
providers.providers.some(({ id }) => typeof id !== "string") ||
!Array.isArray(candidates) ||
candidates.some(
(candidate) =>
typeof candidate?.provider?.id !== "string" ||
candidate.selected !== false,
) ||
providers.analysis_provider_binding !== null ||
providers.analysis_provider_candidates?.find(
({ provider }) => provider?.id === "hopper",
)?.target_support?.status !== "unknown"
candidates.find(({ provider }) => provider?.id === "hopper")?.target_support
?.status !== "unknown"
)
throw new Error("packaged providers CLI failed");
if (process.platform !== "linux") {
+11 -11
View File
@@ -96,7 +96,7 @@ const verifyMcpNonLinuxCurrentDocument = async (
mcpOptions,
current,
) => {
if (json(prompts.mcpText(current)).result !== "fixture")
if (json(prompts.mcpText(current)).normalized_result !== "fixture")
throw new Error("packaged MCP bridge call failed");
const batch = await client.callTool(
{
@@ -105,7 +105,7 @@ const verifyMcpNonLinuxCurrentDocument = async (
},
mcpOptions,
);
const batchResult = json(prompts.mcpText(batch)).result;
const batchResult = json(prompts.mcpText(batch)).normalized_result;
if (
batch.isError === true ||
batchResult?.total !== 1 ||
@@ -175,10 +175,10 @@ const verifyMcpInlineArtifactEvidence = async (
mcpOptions,
);
assert.notEqual(inspected.isError, true, JSON.stringify(inspected));
const source = parseEvidence(inspected.structuredContent?.evidence);
const source = parseEvidence(inspected.structuredContent);
assert.deepEqual(
source.normalized_result,
inspected.structuredContent?.result,
inspected.structuredContent?.normalized_result,
);
assert.deepEqual(
json(prompts.mcpText(inspected)),
@@ -188,17 +188,17 @@ const verifyMcpInlineArtifactEvidence = async (
{
name: "compare_artifacts",
arguments: {
left: inspected.structuredContent.evidence,
right: inspected.structuredContent.evidence,
left: inspected.structuredContent,
right: inspected.structuredContent,
},
},
mcpOptions,
);
assert.notEqual(compared.isError, true, JSON.stringify(compared));
const comparison = parseEvidence(compared.structuredContent?.evidence);
const comparison = parseEvidence(compared.structuredContent);
assert.deepEqual(
comparison.normalized_result,
compared.structuredContent?.result,
compared.structuredContent?.normalized_result,
);
assert.ok(comparison.evidence_links.includes(source.evidence_id));
const bundle = await client.callTool(
@@ -219,12 +219,12 @@ const verifyMcpInlineArtifactEvidence = async (
mcpOptions,
);
assert.notEqual(extracted.isError, true, JSON.stringify(extracted));
const extraction = parseEvidence(extracted.structuredContent?.evidence);
const extraction = parseEvidence(extracted.structuredContent);
assert.deepEqual(
extraction.normalized_result,
extracted.structuredContent?.result,
extracted.structuredContent?.normalized_result,
);
const materialized = extracted.structuredContent?.result;
const materialized = extracted.structuredContent?.normalized_result;
assert.equal(typeof materialized?.output_root, "string");
outputRoot = materialized.output_root;
assert.equal(materialized.containment_verified, true);
+2 -6
View File
@@ -42,12 +42,8 @@ export async function verifyPackagePack({ root, workspace }) {
throw new Error(
"package included authored skill sources instead of only the generated bundle",
);
if (
packedFiles.some((path) =>
/^package\/(?:dist|src)\/generatedMcpToolCatalog\./u.test(path),
)
)
throw new Error("package included the test-only generated MCP catalog");
if (packedFiles.some((path) => path.startsWith("package/.cache/")))
throw new Error("package included development cache or test metadata");
const skill = (
await exec("tar", [
"-xOf",
+17 -10
View File
@@ -31,6 +31,14 @@ const OPENCODE_ORIGINAL = `{
},
}\n`;
const CODEX_ORIGINAL = [
"# Keep Codex preferences and their spelling.",
'model = "gpt-5"',
'notify = ["a", "b"]',
"literal = 'C:\\demo\\path'",
"",
].join("\n");
const verifyMcpAdd = async ({ cli, environment, npxLog }) => {
await run(cli, ["mcp", "add"], environment);
const mcpRegistration = await readFile(npxLog, "utf8");
@@ -248,7 +256,7 @@ const verifySetupPlan = async ({
!planned.plannedActions.some(({ kind }) => kind === "configure_client") ||
!planned.plannedActions.some(({ kind }) => kind === "install_skill") ||
plannedClaudeConfig !== '{"existing":true}\n' ||
plannedCodexConfig !== 'model = "gpt-5"\n' ||
plannedCodexConfig !== CODEX_ORIGINAL ||
plannedCursorConfig !== '{"existing":true}\n' ||
plannedOpenCodeConfig !== opencodeOriginal
)
@@ -459,8 +467,8 @@ const assertCodexSymlink = async ({
!codex.includes("[mcp_servers.rea]") ||
!codex.includes(`command = "${cli}"`) ||
!codex.includes("startup_timeout_sec = 30") ||
(await readFile(`${codexConfig}.rea.backup`, "utf8")) !==
'model = "gpt-5"\n' ||
!codex.startsWith(CODEX_ORIGINAL) ||
(await readFile(`${codexConfig}.rea.backup`, "utf8")) !== CODEX_ORIGINAL ||
(await readFile(`${cursorConfig}.rea.backup`, "utf8")) !==
'{"existing":true}\n'
)
@@ -631,7 +639,6 @@ const verifyUninstall = async ({
codexTarget,
opencodeConfig,
}) => {
const openCodeBeforeUninstall = await readFile(opencodeConfig, "utf8");
const uninstallExecution = await runWithStatus(
cli,
["uninstall", "--json"],
@@ -657,7 +664,7 @@ const verifyUninstall = async ({
!(await lstat(codexConfig)).isSymbolicLink() ||
cursorAfterUninstall.existing !== true ||
cursorAfterUninstall.mcpServers?.rea !== undefined ||
!codexAfterUninstall.includes('model = "gpt-5"') ||
codexAfterUninstall !== CODEX_ORIGINAL ||
codexAfterUninstall.includes("mcp_servers.rea") ||
openCodeErrors.length !== 0 ||
openCodeAfterUninstall?.model !== "provider/model" ||
@@ -670,11 +677,10 @@ const verifyUninstall = async ({
"Keep this independently managed server",
) ||
(await readFile(`${opencodeConfig}.rea.backup`, "utf8")) !==
openCodeBeforeUninstall ||
!(await readFile(`${cursorConfig}.rea.backup`, "utf8")).includes('"rea"') ||
!(await readFile(`${codexConfig}.rea.backup`, "utf8")).includes(
"[mcp_servers.rea]",
)
OPENCODE_ORIGINAL ||
(await readFile(`${cursorConfig}.rea.backup`, "utf8")) !==
'{"existing":true}\n' ||
(await readFile(`${codexConfig}.rea.backup`, "utf8")) !== CODEX_ORIGINAL
)
throw new Error(
`packaged uninstall did not preserve config symlinks: ${JSON.stringify(uninstall)}`,
@@ -704,6 +710,7 @@ export async function verifyPackageSetup({
);
const siblingSkillPath = join(home, ".agents/skills/unrelated/SKILL.md");
if (supportedSetupHost) {
await writeFile(codexTarget, CODEX_ORIGINAL);
await mkdir(join(home, ".agents/skills/unrelated"), { recursive: true });
await writeFile(siblingSkillPath, "unrelated skill\n");
}
+11 -6
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import {
@@ -171,7 +172,14 @@ try {
})
).stdout,
);
assert.ok(JSON.stringify(providers).includes("ghidra"));
assert.equal(providers.analysis_provider_binding, null);
assert.ok(Array.isArray(providers.analysis_provider_candidates));
const ghidra = providers.analysis_provider_candidates.find(
({ provider }) => provider.id === "ghidra",
);
assert.ok(ghidra !== undefined);
assert.equal(ghidra.selected, false);
assert.equal(ghidra.target_support.status, "unknown");
report.cli.providers = true;
const inspected = await exec(
process.execPath,
@@ -210,12 +218,9 @@ try {
{ timeout: 360_000 },
);
assert.notEqual(result.isError, true, `Packaged operation failed: ${name}`);
const structured =
result.structuredContent ??
JSON.parse(result.content.find((item) => item.type === "text").text);
const parsed = contract.outputSchema.parse(structured);
const projected = requireMcpOperationResult(result, name, contract);
report.mcpOperations.push(name);
return parsed.result;
return projected;
};
try {
await client.connect(transport);
+15 -15
View File
@@ -10,7 +10,7 @@ import { fileURLToPath } from "node:url";
import { CdpBrowserProvider } from "../dist/browser/CdpBrowserProvider.js";
import { waitForBrowserDevtoolsPort } from "../dist/browser/BrowserProcessStartup.js";
import { PlaywrightBrowserScenarioProvider } from "../dist/browser/PlaywrightBrowserScenarioProvider.js";
import { createBrowserScenarioProvider } from "../dist/composition/browserScenario.js";
import {
inspectWebPageInputSchema,
listBrowserTargetsInputSchema,
@@ -240,10 +240,9 @@ try {
try {
attachedScenario = await runScenarioCli(attachedScenarioInput);
} catch (cliError) {
const direct =
await new PlaywrightBrowserScenarioProvider().captureScenario(
attachedScenarioInput,
);
const direct = await createBrowserScenarioProvider(
process.env,
).captureScenario(attachedScenarioInput);
if (!direct.ok) {
const underlying = direct.error.cause;
const details =
@@ -274,16 +273,17 @@ try {
throw new Error("Scenario attachment terminated its external browser");
const profilesBefore = await scenarioProfiles();
const launchedScenario =
await new PlaywrightBrowserScenarioProvider().captureScenario(
browserScenario(
{
mode: "launch",
executable_path: executable,
},
site.origin,
),
);
const launchedScenario = await createBrowserScenarioProvider(
process.env,
).captureScenario(
browserScenario(
{
mode: "launch",
executable_path: executable,
},
site.origin,
),
);
if (!launchedScenario.ok) throw launchedScenario.error;
if (
launchedScenario.value.browser.cleanup !== "terminated-owned-process" ||
+3 -1
View File
@@ -50,7 +50,9 @@ const input = electronActiveObservationInputSchema.parse({
],
});
const verifierRun = createVerifierRun();
const result = await new PlaywrightElectronActiveProvider().capture(input);
const result = await new PlaywrightElectronActiveProvider(process.env).capture(
input,
);
if (!result.ok) throw result.error;
const output = {
+10 -5
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
@@ -28,6 +29,7 @@ if (process.env.GHIDRA_INSTALL_DIR === undefined)
"verify:ghidra:com prerequisite missing: GHIDRA_INSTALL_DIR (bring your own Ghidra).",
);
const installation = inspectGhidraInstallation({
environment: process.env,
installDir: process.env.GHIDRA_INSTALL_DIR,
...(process.env.JAVA_HOME === undefined
? {}
@@ -331,12 +333,15 @@ async function call(name, args = {}) {
{ timeout: 240000 },
);
assert.notEqual(value.isError, true, JSON.stringify(value));
if (name === "open_binary") return value.structuredContent.result;
if (name === "close_binary") return value.structuredContent;
const evidence = parseEvidence(value.structuredContent.evidence);
const projected = requireMcpOperationResult(value, name);
if (name === "open_binary" || name === "close_binary") return projected;
const evidence = parseEvidence(value.structuredContent);
assert.equal(evidence.subject.digest.sha256, sha256);
assert.deepEqual(evidence.normalized_result, value.structuredContent.result);
return value.structuredContent.result;
assert.deepEqual(
evidence.normalized_result,
value.structuredContent.normalized_result,
);
return projected;
}
async function cli(command, address, options = []) {
const { stdout } = await promisify(execFile)(
+5 -9
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import {
@@ -38,6 +39,7 @@ if (process.env.GHIDRA_INSTALL_DIR === undefined)
"verify:ghidra:dos prerequisite missing: GHIDRA_INSTALL_DIR (bring your own Ghidra).",
);
const installation = inspectGhidraInstallation({
environment: process.env,
installDir: process.env.GHIDRA_INSTALL_DIR,
...(process.env.JAVA_HOME === undefined
? {}
@@ -552,11 +554,8 @@ async function call(name, arguments_ = {}) {
{ timeout: 240000 },
);
assert.notEqual(result.isError, true, JSON.stringify(result));
assert.ok(
result.structuredContent && "result" in result.structuredContent,
`${name} omitted structured result`,
);
return result.structuredContent.result;
assert.ok(result.structuredContent, `${name} omitted structured result`);
return requireMcpOperationResult(result, name);
}
async function evidenceCall(name, arguments_) {
const result = await client.callTool(
@@ -564,10 +563,7 @@ async function evidenceCall(name, arguments_) {
{ timeout: 240000 },
);
assert.notEqual(result.isError, true, JSON.stringify(result));
const evidence = parseEvidence({
...result.structuredContent?.evidence,
normalized_result: result.structuredContent?.result,
});
const evidence = parseEvidence(result.structuredContent);
assert.equal(evidence.subject.digest.sha256, sha256);
assert.equal(evidence.provider.id, "ghidra");
assertProfile(evidence.analysis_profile);
+1 -2
View File
@@ -92,8 +92,7 @@ export async function verifyDebugFunctionOperations(
if (
instruction.status !== "decoded" ||
instruction.length < 1 ||
instruction.bytes.length !== instruction.length * 2 ||
instruction.operands.length === 0
instruction.bytes.length !== instruction.length * 2
)
throw new Error("Structured native instruction facts drifted");
const directSite = outgoing.references.find(
+6 -4
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import {
@@ -392,12 +393,13 @@ async function call(name, args = {}) {
{ timeout: 360000 },
);
assert.notEqual(response.isError, true, JSON.stringify(response));
if (name === "close_binary") return response.structuredContent;
const evidence = parseEvidence(response.structuredContent.evidence);
const projected = requireMcpOperationResult(response, name);
if (name === "close_binary") return projected;
const evidence = parseEvidence(response.structuredContent);
assert.equal(evidence.subject.digest.sha256, sha256);
assert.deepEqual(
evidence.normalized_result,
response.structuredContent.result,
response.structuredContent.normalized_result,
);
return response.structuredContent.result;
return projected;
}
+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { copyFile, mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js";
import { toolContract } from "../dist/contracts/toolContracts.js";
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
const execute = promisify(execFile);
const run = createVerifierRun();
const installation = inspectGhidraInstallation({
environment: process.env,
installDir: process.env.GHIDRA_INSTALL_DIR,
javaHome: process.env.JAVA_HOME,
});
if (process.platform !== "darwin" || installation.status !== "available")
throw new Error(
"verify:ghidra:swift requires macOS, Ghidra 12.1.4 with its native decompiler, and JDK 21; set GHIDRA_INSTALL_DIR and JAVA_HOME.",
);
try {
await execute("swiftc", ["--version"], { timeout: 10_000 });
} catch (cause) {
throw new Error(
"verify:ghidra:swift requires the host Swift compiler (swiftc).",
{ cause },
);
}
const root = await mkdtemp(join(tmpdir(), "rea-ghidra-swift-"));
const client = new Client({ name: "ghidra-swift-verifier", version: "1" });
const transport = new StdioClientTransport({
command: process.execPath,
args: [fileURLToPath(new URL("./rea.mjs", import.meta.url)), "mcp"],
env: Object.fromEntries(
Object.entries(process.env).filter(([, value]) => value !== undefined),
),
stderr: "pipe",
});
const source = fileURLToPath(
new URL("../tests/conformance/swift/type-inventory.swift", import.meta.url),
);
const binary = join(root, "swift-inventory");
const reports = [];
try {
await execute("swiftc", ["-O", "-g", source, "-o", binary], {
timeout: 60_000,
});
await mkdir(join(root, "copied"));
const copied = join(root, "copied", "swift-inventory");
await copyFile(binary, copied);
await client.connect(transport);
for (const [variant, path] of [
["debug", binary],
["without-dsym", copied],
]) {
await call("open_binary", { path, provider_id: "ghidra" });
try {
const procedures = await call("list_procedures", {});
const inventory = await call("analyze_swift_types", {});
assert(
inventory.total > 0,
"Swift procedures disappeared from the inventory",
);
assert(
inventory.categories.other.count > 0,
"Top-level Swift symbols were lost",
);
const structs = await call("analyze_swift_types", {
category: "structs",
});
assert(
structs.categories.structs.count > 0,
"Modern struct symbols were lost",
);
const metadata = await call("analyze_swift_types", {
category: "structs",
pattern: "VMa",
});
assert.equal(
metadata.total,
2,
"Pair and Scorer metadata accessors must both be classified",
);
const pair = await call("analyze_swift_types", {
category: "structs",
pattern: "Pair",
});
assert(pair.total > 0, "Literal name filtering lost Pair symbols");
const unresolved = await call("analyze_swift_types", {
category: "structs",
pattern: "swiftIndirect",
});
assert(
unresolved.unclassified.length > 0,
"Unsupported categories must remain explicit",
);
const entries = Object.values(inventory.categories).flatMap(
(category) => category.items,
);
assert.equal(
new Set(entries.map((entry) => entry.name)).size,
inventory.total,
);
assert(
!entries.some(
(entry) =>
entry.name === "entry" || entry.name === "_swift_allocObject",
),
);
for (const entry of entries)
assert(
procedures.some(
(procedure) =>
procedure.address === entry.address &&
procedure.value === entry.name,
),
);
reports.push({
variant,
total: inventory.total,
structs: structs.total,
metadata_accessors: metadata.total,
unclassified: inventory.unclassified.length,
});
} finally {
await call("close_binary", {});
}
}
} finally {
await client.close();
await transport.close();
await rm(root, { recursive: true, force: true });
}
console.log(
JSON.stringify(
{
ok: true,
provider: "ghidra",
fixtures: reports,
verifier_run: await completeVerifierRun(run),
},
null,
2,
),
);
async function call(name, arguments_) {
const response = await client.callTool(
{ name, arguments: arguments_ },
{ timeout: 180_000 },
);
if (name === "analyze_swift_types")
toolContract(name).outputSchema.parse(response.structuredContent);
return requireMcpOperationResult(response, name);
}
+24 -15
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env node
import assert from "node:assert/strict";
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import {
@@ -74,6 +75,7 @@ if (!compilerOnly) {
"Ghidra switch lane prerequisite missing: GHIDRA_INSTALL_DIR (bring your own)",
);
installation = inspectGhidraInstallation({
environment: process.env,
installDir: process.env.GHIDRA_INSTALL_DIR,
...(process.env.JAVA_HOME === undefined
? {}
@@ -237,15 +239,21 @@ async function verifyTarget(path, sha256, fixtures, variant) {
const observed = await toolResult(client, "inspect_native_api", {
procedure: procedure.address,
});
assertWorkflowIdentity(observed.evidence, sha256);
assert.equal(observed.result.procedure.address, procedure.address);
assertWorkflowIdentity(observed, sha256);
assert.equal(
observed.normalized_result.procedure.address,
procedure.address,
);
const summary = assertSwitchBoundary(
observed.result.boundary,
observed.normalized_result.boundary,
observationFixture,
);
assert.deepEqual(
[...observed.result.residual_unknowns].sort(),
expectedResidualUnknowns(observed.result.boundary, fixture).sort(),
[...observed.normalized_result.residual_unknowns].sort(),
expectedResidualUnknowns(
observed.normalized_result.boundary,
fixture,
).sort(),
"Residual unknowns must preserve unresolved ABI types and exactly the expected switch uncertainty",
);
const cli = await cliSwitchEvidence(
@@ -259,20 +267,20 @@ async function verifyTarget(path, sha256, fixtures, variant) {
assertSwitchBoundary(cli.normalized_result.boundary, observationFixture);
assert.deepEqual(
cli.normalized_result.boundary.jump_tables,
observed.result.boundary.jump_tables,
observed.normalized_result.boundary.jump_tables,
"CLI/MCP switch mappings/defaults/data-source evidence differ",
);
assert.deepEqual(
cli.normalized_result.residual_unknowns,
observed.result.residual_unknowns,
observed.normalized_result.residual_unknowns,
);
observations.push({
name: fixture.name,
...summary,
cli_mcp_equal: true,
limitations: observed.result.boundary.limitations,
residual_unknowns: observed.result.residual_unknowns,
jump_tables: observed.result.boundary.jump_tables,
limitations: observed.normalized_result.boundary.limitations,
residual_unknowns: observed.normalized_result.residual_unknowns,
jump_tables: observed.normalized_result.boundary.jump_tables,
});
}
return { observations, stderr };
@@ -319,14 +327,15 @@ async function toolResult(client, name, arguments_ = {}) {
{ timeout: 240000 },
);
assert.notEqual(result.isError, true, JSON.stringify(result));
assert.ok(
result.structuredContent && "result" in result.structuredContent,
`${name} omitted structured result`,
);
assert.ok(result.structuredContent, `${name} omitted structured result`);
return result.structuredContent;
}
async function call(client, name, arguments_ = {}) {
return (await toolResult(client, name, arguments_)).result;
const result = await client.callTool(
{ name, arguments: arguments_ },
{ timeout: 240000 },
);
return requireMcpOperationResult(result, name);
}
async function cliSwitchEvidence(entrypoint, path, procedure, env) {
+14 -1
View File
@@ -48,6 +48,7 @@ if (installDir === undefined || !isAbsolute(installDir))
"Set GHIDRA_INSTALL_DIR to the absolute root of Ghidra 12.1.4.",
);
const installation = inspectGhidraInstallation({
environment: process.env,
installDir,
...(process.env.JAVA_HOME === undefined
? {}
@@ -94,6 +95,7 @@ if (!profile.ok || profile.value.profile === null)
const client = new GhidraClient({
platform: installation.platform,
launcher: new GhidraHeadlessLauncher({
environment: process.env,
analyzeHeadlessPath: installation.analyzeHeadlessPath,
...(process.env.JAVA_HOME === undefined
? {}
@@ -112,6 +114,7 @@ const client = new GhidraClient({
let coordinates;
let report;
let primaryFailure;
const observed = new Set();
try {
const started = await client.start();
@@ -257,8 +260,18 @@ try {
"no-gui-or-mutation-authority",
],
};
} catch (cause) {
primaryFailure = cause;
throw cause;
} finally {
await client.close();
const closed = await client.close();
if (!closed.ok)
throw primaryFailure === undefined
? closed.error
: new AggregateError(
[primaryFailure, closed.error],
"Ghidra verification and cleanup failed",
);
if (coordinates !== undefined) await assertCleanup(coordinates);
}
if (report === undefined)
+14 -1
View File
@@ -50,6 +50,7 @@ if (installDir === undefined || !isAbsolute(installDir))
"Set GHIDRA_INSTALL_DIR to the absolute root of an extracted Ghidra 12.1.4 release.",
);
const installation = inspectGhidraInstallation({
environment: process.env,
installDir,
...(process.env.JAVA_HOME === undefined
? {}
@@ -442,6 +443,7 @@ async function verifyTarget(targetPath, variant, expectedTarget = null) {
const client = new GhidraClient({
launcher: new GhidraHeadlessLauncher({
environment: process.env,
analyzeHeadlessPath: installation.analyzeHeadlessPath,
...(process.env.JAVA_HOME === undefined
? {}
@@ -458,6 +460,7 @@ async function verifyTarget(targetPath, variant, expectedTarget = null) {
});
let runtimeCoordinates;
let primaryFailure;
try {
const started = await client.start();
if (!started.ok) throw started.error;
@@ -539,8 +542,18 @@ async function verifyTarget(targetPath, variant, expectedTarget = null) {
native_api_cli: nativeApiCli,
native_values: nativeValues,
};
} catch (cause) {
primaryFailure = cause;
throw cause;
} finally {
await client.close();
const closed = await client.close();
if (!closed.ok)
throw primaryFailure === undefined
? closed.error
: new AggregateError(
[primaryFailure, closed.error],
"Ghidra verification and cleanup failed",
);
if (runtimeCoordinates !== undefined)
await assertCleanup(runtimeCoordinates);
}

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