Compare commits

..
Author SHA1 Message Date
N0zoM1z0andKaoru0822 d3062c207c chore: validate optimization incorporated into current main
Main 1b4639879 incorporates the original six-file patch; retain subsequent independent mutation workflow and cache-output guard improvements.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-14 12:59:22 +00:00
3b79af28e8 chore(main): release rea-agents 6.5.0 (#1693)
* chore(main): release rea-agents 6.5.0

* docs(release): finalize contributor-aware 6.5.0 notes

* docs(release): retain verifier maintenance only in provenance

* style(release): format the contribution inventory

* docs(release): credit Windows fixes

* fix(comparison): recheck historical calls-only changes (#2223)

* fix(comparison): preserve incomplete function call evidence

Keep missing, unknown, partial and unresolved call relationships unknown in function comparisons and reconstruction claims. Aggregate IDA caller and callee coverage without overwriting uncertainty.

Validate with 144 focused tests, typecheck/lint, docs checks and test discovery. Real providers were not verified; the full headless IDA session suite requires unavailable Windows native controls.

* fix(comparison): recheck historical calls-only changes

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>

* fix(ci): isolate Linux Hopper process fixture inventory

* docs(release): credit selected-null and client setup changes

* fix(release): refresh 6.5.0 notes and align installation examples

* docs(release): clarify 6.5.0 migrations and recent fixes

* docs(release): cover omitted capabilities and refresh 6.5.0 provenance

* docs(release): pin final CI corrections in 6.5.0 inventory

* docs(release): include mutation scheduling in 6.5.0 provenance

* docs(release): include cache policy correction in 6.5.0 provenance

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
Co-authored-by: Codex <codex@users.noreply.github.com>
Co-authored-by: Lucien <164857476+dakjdakd@users.noreply.github.com>
2026-10-14 20:31:45 +08:00
morluto cbc0e97c29 test(ci): bind npm cache IDs to output consumers
Remove the blanket caller ID requirement that rejected the standalone mutation workflow. Keep producer bindings where static cache authorization and package verification consume action outputs.
2026-10-14 20:10:17 +08:00
morluto 92a0edf187 perf(ci): move full mutation sweeps off the release path
Keep fresh coverage and both real mutation preflights required in every CI tier. Run complete canonical sweeps daily or manually in isolated jobs with distinct reports, retained failure diagnostics, and explicit report-presence checks.

Preserve regression inventories, release qualification and mutation settings; remove the superseded duplicate gate test. Related: #3281.
2026-10-14 19:49:08 +08:00
N0zoM1z0andKaoru0822 1b46398795 perf(ci): preflight mutation configurations during fast feedback
Keep full qualification and standalone mutation execution intact, retain distinct reports, and preserve fail-closed quality gates.

Related: #3281

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-14 19:48:30 +08:00
N0zoM1z0andKaoru0822 07da653ae5 chore: retain current upstream Hopper discovery contract
Discard the superseded restoration and retain upstream 0686b0754. Only the six CI optimization files remain relative to main f75b1a416.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-14 11:26:57 +00:00
morluto f75b1a416b test(browser): retain bounded diagnostics for scenario policy failures 2026-10-14 19:24:14 +08:00
morluto 0686b07546 test(doctor): retain discovered Hopper paths despite version failures 2026-10-14 19:23:11 +08:00
N0zoM1z0andKaoru0822 f3602a4b3b chore: integrate verified main before CI optimization review
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-14 11:16:32 +00:00
N0zoM1z0andKaoru0822 e2259b5264 chore: integrate current release-notes changes before verification
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-14 11:15:16 +00:00
morluto c2b79aa462 test(doctor): separate Hopper availability and version assertions 2026-10-14 19:10:22 +08:00
N0zoM1z0andKaoru0822 e8c0ecfd47 Integrate the platform-contract restoration before final CI review
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-14 11:07:53 +00:00
morluto efe0923914 fix(release): exclude Codex automation from contributor thanks 2026-10-14 19:06:36 +08:00
N0zoM1z0andKaoru0822 3698e4f899 test(hopper): retain the updated platform-specific doctor contract
Main changed the same fixture before #3299 merged. Remove the obsolete synthetic loader setup, preserve main native platform readiness and exit assertions, and keep the Rizin readiness fix intact.

Fixes #3406

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-14 10:51:58 +00:00
YsopwnableandKaoru0822 a32b4b6ac5 test(ci): correct Rizin readiness and Hopper launcher fixtures (#3299)
Correct two independently reproduced CI fixture preconditions: close the real Rizin child's stdin before its readiness frame, and provide scoped Linux loader evidence for the Hopper doctor shell fixture. Preserve original timeouts, rejection checks, path spelling, launch prevention and cleanup.

Current-head CI passed, including both complete reverse-tools mutation passes and 1,273 unique portable test files (14,521 passed, 126 skipped, zero failures). Each issue retains its own branch/commit; the validated integration tree includes the Hopper fix from #3410. That duplicate PR can close after the same code is verified on main.

Fixes #3295
Fixes #3406

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-14 18:42:33 +08:00
morluto 60c08802a7 test(native): validate demangling through owned contracts 2026-10-14 18:14:01 +08:00
morluto 7435e79d65 test(doctor): assert platform-specific Hopper readiness and exit status 2026-10-14 18:14:01 +08:00
N0zoM1z0andKaoru0822 117f2e4476 perf(ci): preflight mutation configurations during fast feedback
Keep full qualification and standalone mutation execution intact, retain distinct reports, and preserve fail-closed quality gates.

Fixes #3281

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-14 10:10:36 +00:00
N0zoM1z0andKaoru0822 2a2c9fa574 Adopt the verified upstream semantic correction before final CI profiling
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-14 10:00:22 +00:00
morluto 44d98de236 test: align malformed XML and route seed assertions with owning contracts 2026-10-14 17:55:22 +08:00
N0zoM1z0andKaoru0822 a89ce212d6 Merge latest main and retain the upstream wrapper ownership correction
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-14 09:54:22 +00:00
morlutoandmsaimshafique ba33c706d2 fix(setup): inspect installed skill identity for unqualified doctor
Select installed skill destinations in the existing inspection pass, retaining malformed or foreign identity facts while ignoring empty marker-only roots.

Refs: #3208, #3296

Co-authored-by: msaimshafique <msaimshafique4664@gmail.com>
2026-10-14 17:49:20 +08:00
morlutoandhaoran20100322 aade9bbcb7 fix(setup): evaluate Gemini workspace trust before settings content
Treat only ENOENT as absence. Preserve unreadable metadata errors and ignore untrusted workspace content before parsing it.

Refs: #3206, #3319

Co-authored-by: haoran20100322 <haoran20100322@outlook.com>
2026-10-14 17:49:19 +08:00
morlutoandPaoloValletta 0930e94e07 fix(setup): validate registered MCP output schema profiles
Reuse the shared profile schema before setup writes and preserve supported values through registration and startup.

Refs: #3205, #3277

Co-authored-by: PaoloValletta <paovalletta@hotmail.it>
2026-10-14 17:49:19 +08:00
morluto 3f6b5755b3 fix(managed)!: share member list facts and retain verification uncertainty
Parse TypeDef member lists once at their owning boundary, retain malformed ranges as unknown, and admit representation storage before allocation. Reuse parsed facts across member ownership, inventory, attributes, and native boundaries. Preserve constant operands in structural matching.

Refs: #3202, #3201, #3199

BREAKING CHANGE: unresolved managed/native verification results may retain a non-null matched_native and candidate facts when module identity is unknown. Consumers must use status and reason rather than assume unresolved observations are empty.
2026-10-14 17:49:18 +08:00
morluto 45b6a7fe8f fix(native): distinguish proven pre-launch failure from unknown target ownership
Emit explicit target-creation facts only when known. Invalid post-launch process handles remain unknown and retain cleanup ownership.

Refs: #3185

Validation uses bridge fakes and provider-boundary tests; no real LLDB target launch was performed.
2026-10-14 17:49:18 +08:00
morlutoandSII-YDD 2daeff4b24 fix(application): normalize owned target admission cancellation
Normalize only the exact aborted signal reason, retaining unrelated failures and descriptor ownership after cleanup failures.

Refs: #3197, #3265

Co-authored-by: SII-YDD <38822679+SII-YDD@users.noreply.github.com>
2026-10-14 17:49:18 +08:00
morlutoandhhang e67c3642c8 fix(process): retain deletions after observed roots become obstructed
Carry initial root observations into final capture. Treat ENOTDIR as absence only for a previously observed root, preserving initial obstruction and cancellation failures.

Refs: #3238, #3263

Co-authored-by: hhang <169027046+xu91102@users.noreply.github.com>
2026-10-14 17:49:17 +08:00
morluto 030c477328 fix(evidence): derive bundle comparison status from classified changes
Keep digests as provenance while status follows examined deltas and unresolved coverage.

Refs: #3216
2026-10-14 17:49:17 +08:00
morluto 3a34ad37bd fix(evidence): classify noncanonical bundles at the input boundary
Use a dedicated canonical-projection error so unrelated TypeErrors retain their protocol failure meaning.

Refs: #3219
2026-10-14 17:49:16 +08:00
morluto 1250aaedd8 fix(browser): decode SAZ XML through typed encoding validation
Reuse the format-aware XML text owner, preserve decoder causes, and reject contradictory declarations even in unselected records.

Refs: #3273
2026-10-14 17:49:16 +08:00
morluto 2b60e097f2 fix(browser): preserve uncertainty and compare redirect observations
Retain scenario selection limitations independently of known changes. Compare known status, MIME and byte observations for each redirect hop without inventing missing facts.

Refs: #3172, #3173
2026-10-14 17:49:15 +08:00
morluto 5e79d468e5 fix(javascript): retain runtime candidate ambiguity
Keep referenced ambiguous candidates unknown instead of classifying them as absent.

Refs: #3192
2026-10-14 17:49:15 +08:00
morlutoandJoão Pedro de Faria 44c9d2924d fix(javascript): match feature seeds against their owning source values
Apply exact, contains, and case options to node IDs. Route seeds consume retained route values rather than generated display labels.

Refs: #3189, #3190, #3298

Co-authored-by: João Pedro de Faria <jpdefo7@hotmail.com>
2026-10-14 17:49:14 +08:00
morluto 503109995f fix(javascript): retain early-return conditions in wrapped call paths
Share sequential fallthrough interpretation between block traversal and isolated seed context.

Refs: #3196
2026-10-14 17:49:14 +08:00
morluto 28f5761973 test(mcp): isolate target-free sessions from host provider discovery
Inject explicit unavailable provider ports into the fake-session harness, forbid accidental execution, and retain public availability coverage. This removes host-dependent timeouts without increasing deadlines.
2026-10-14 17:49:13 +08:00
morlutoandhhang dae1e55381 fix(javascript): preserve lexical binding and default export capture semantics
Resolve CommonJS wrapper bindings from the parser-owned source type. Capture default identifier export values at the export statement while preserving live named bindings.

Refs: #3261, #3297

Co-authored-by: hhang <169027046+xu91102@users.noreply.github.com>
2026-10-14 17:49:13 +08:00
N0zoM1z0andKaoru0822 a37755d57c Integrate the separately reviewed main test-fixture prerequisites
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-14 09:44:28 +00:00
N0zoM1z0andKaoru0822 74195532be test(hopper): provide controlled Linux loader evidence for doctor
Fixes #3406

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-14 09:43:37 +00:00
N0zoM1z0andKaoru0822 f62632e7a6 test(javascript): assert wrapper ownership in the correct source mode
Fixes #3341

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-14 09:17:25 +00:00
morlutoandGeronimo 322506d0c0 fix(javascript): retain unavailable package metadata during resolution
Consume the same-container inventory lookup before treating a missing package.json projection as absent. Unknown metadata prevents index fallback and unsupported bare-package exports resolution; restored metadata selects its declared main or import/require export.

Validation: owner same-container/restored controls and actual ASAR CLI missing/restored package-manifest workflow. Completes the P1 review finding on #3278 that remained after the earlier missing-companion projection fix.

Fixes #3213
Related: #3278

Co-authored-by: Geronimo <varungore1@gmail.com>
2026-10-14 17:09:18 +08:00
7582f0dff2 fix(hopper): share launcher discovery across setup and runtime
Centralize canonical system/user and deterministic renamed-bundle path facts. Reuse the installer destination and honor the same local candidates in doctor and runtime; scan renamed bundles only after canonical candidates fail. Preserve explicit path spelling and exclusive selection. Homebrew command probing remains doctor-only with explicit runtime configuration guidance.

Validation: owner, config, setup and doctor tests cover canonical priority and renamed paths; public CLI verifies explicit path preservation without launching Hopper. No real Hopper analysis claimed.

Fixes #3054
Related: #3085

Co-authored-by: Vinícius DS Andrade <95101505+viniciusdsandrade@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-14 17:09:17 +08:00
morluto 864314ac85 fix(adb)!: distinguish reinstall intent from observed replacement
Preserve requested adb reinstall mode as replace_requested and report replaced as null when no prior package-state observation exists. Keep install Success/Failure, raw output and host digest, with an explicit Evidence limitation; add no device queries.

Validation: stateful CLI fake distinguishes fresh/existing installs and checks exact version/install commands, plus provider and MCP schema controls. No real-device verification claimed.

Fixes #3166

BREAKING CHANGE: install_adb_package now requires replace_requested and exposes replaced as nullable rather than claiming the selected mode was an observed replacement.
2026-10-14 17:09:17 +08:00
N0zoM1z0andKaoru0822 7a9991289c Merge latest main to include the verified uninstall fixture corrections
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-14 08:59:04 +00:00
morluto 0351328e54 fix(electron): retain nested bridge facts through wrapper resolution
Merge direct same-call nested member facts into symbolic wrapper results using canonical deduplication and ordering. Preserve unknown spread coverage rather than losing known member paths.

Validation: bounded source cases and public CLI/stdio MCP spread/no-spread controls. The existing 125,000-member stress case exceeded the 384 MiB local cap and was stopped without increasing the limit; that capacity case remains unverified locally.

Fixes #3212
2026-10-14 16:54:08 +08:00
morluto 81f7638203 fix(javascript): keep exports out of write relations
Project writes only from write-role references. Export references remain distinct facts and genuine assignments still traverse writes-only traces.

Validation: source semantic controls and public CLI/MCP writes-only traces.

Fixes #3207
2026-10-14 16:54:08 +08:00
7e5cffa2ae fix(javascript): preserve loader ownership and unavailable artifact facts
Preserve missing ASAR companion occurrence identity, declared size and unknown content in graph nodes and resolution limitations. Apply URL text semantics once for ESM lookup while preserving literal CommonJS paths and original specifiers. Keep CJS wrapper var bindings and assignment history, recognizing only untouched wrapper redeclarations.

Validation: real ASAR producer workflow and CLI/MCP path/coverage cases; semantic reassignment controls. Context7 Node documentation and direct Node wrapper execution confirm wrapper behavior.

Fixes #3213
Fixes #3214
Fixes #3223
Related: #3278, #3254

Co-authored-by: Geronimo <varungore1@gmail.com>
Co-authored-by: Paolo Valletta <paovalletta@hotmail.it>
2026-10-14 16:54:08 +08:00
morluto 1d826f304f fix(process): bound normalized terminal retention before allocation
Pre-count ordered normalization stages before materializing output. Charge original and normalized terminal bytes independently, omit whole over-budget chunks and preserve exact raw counts plus explicit unknown final sizes. Bound rendered normalization, retain legacy capture compatibility, and remove temporary per-match output arrays.

Validation: UTF-8 surrogate boundaries, shrinking/no-op/literal rules, capture validation and public MCP expansion/equal-size controls with verified cleanup. CPU, process-family memory and token changes are unmeasured; no performance speedup claimed. Nonterminal derived payload bounds remain separate work.

Fixes #3168
2026-10-14 16:54:08 +08:00
morlutoandCRYPTONIKAV 7fb0a0cc57 fix(mcp): bound SDK validation errors before transport delivery
Measure actual wire result JSON, including its provenance marker, without allocating the full serialization. Retain complete SDK diagnostics through the existing Evidence writer and return a bounded reference or explicit retention failure. Keep fitting escaped errors and subsequent valid calls usable.

Validation: actual SDK client/transport retention readback and fit/one-byte-over delivery boundaries.

Fixes #3221
Related: #3267

Co-authored-by: CRYPTONIKAV <29835619+CRYPTONIKAV@users.noreply.github.com>
2026-10-14 16:54:08 +08:00
morlutoandTaksh e5deb832cb fix(android): report validated ADB and Apktool host availability
Probe the selected clients through their owning providers and expose current host availability through binary_session. Keep device/serial and target applicability separate. Reject unrecognized successful version output; retain the bounded producer output and original version spelling. Apply the documented Apktool floor, including the verified Debian dirty build.

Validation: provider/version tests and public MCP grouped availability; no real Android device or APK provider verification claimed.

Fixes #3067
Fixes #3068
Related: #3077

Co-authored-by: Taksh <takshkothari09@gmail.com>
2026-10-14 16:54:08 +08:00
morlutoandakushonkamen cf806ea02a fix(hopper): prepare deterministic FAT32 analysis slices
Use the validated thin-image preparation path for FAT32 and FAT64. Prefer ordinary arm64 then arm64e, masking capability bits only for ranking while preserving raw slice identity; reject unresolved ambiguity. Bump the analysis profile epoch.

Validation: slice/profile fixtures cover raw subtypes, ambiguity, malformed tables and source integrity. Real Hopper execution was not run.

Related: #3072, #3092

Co-authored-by: akushonkamen <akushonkamen@163.com>
2026-10-14 16:54:08 +08:00
morluto ee5cfbe05c fix(dotnet): accept legacy small exception section sizes
Apply CoreCLR-compatible clause counting while validating and advancing by the physical section extent. Preserve malformed, reserved, chained and truncation checks.

Validation: managed decoder suite passes; no native compiler or runtime verification claimed.

Fixes #3279
2026-10-14 16:54:08 +08:00
morluto 459c0951f2 fix(analysis): propagate typed cancellation across batch stages
Stop dispatching later resolution, identity and pseudocode stages after typed cancellation; settle already dispatched work. Preserve ordinary partial failures and stop queued graph traversal.

Validation: public MCP cancellation and partial-failure controls.

Fixes #3198
2026-10-14 16:54:07 +08:00
morluto a154b9752c test(setup): use verified invocations in uninstall fixtures
Update byte-preservation fixtures for the fail-closed command ownership contract. Unknown path retention remains covered.

Validation: both filesystem boundary files pass, including BOM restoration and idempotent removal.
2026-10-14 16:54:07 +08:00
Wade ffeb2d4367 fix(apple): keep unrooted extensionless Mach-O in IPA executables (#3300)
IPA projections keep components outside every application root, but
executables required root membership and native_libraries matched only
.dylib/.so names, so an unrooted extensionless Mach-O such as
SwiftSupport/HelperTool was in neither list and formed no bridge
candidate. List unrooted Mach-O images under executables unless they
are already native libraries.

Fixes #3174
2026-10-14 16:52:32 +08:00
morluto 5f0065c542 ci: validate initial PR readiness declarations (#3302) 2026-10-14 16:51:59 +08:00
N0zoM1z0andKaoru0822 82f9b42996 test(rizin): close fixture stdin before announcing readiness
Fixes #3295

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-14 08:39:32 +00:00
7c962cc70b feat(website): simplify the reader journey and beginner setup (#3274)
Lead new readers from compact setup through the preserved dinosaur and
Calculator examples, with code, comparisons, and verification in disclosures.
Expand Windows, macOS, and Linux onboarding, client/provider choices,
on-site curl instructions, and the supplied first investigation.
Complete Get started translations while retaining executable literals,
historical evidence, and main's explicitly partial Greek fallback.

Verified the integrated website, its 12 maintenance regressions, all tracked
formatting, typecheck/lint, 38 locale/base-path browser combinations, and
the public Funnel preview. Native installers and providers were not run.
The separate manual production publication workflow is not triggered here.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Assisted-by: Suika (gensokyo-skills:suika-scatter-gather-planning)
2026-10-14 08:36:00 +00:00
Wade 57290a01ac fix(apple): order projection paths by Unicode code point (#3294)
The Apple projection sorted application roots, bundle paths and symlinks
with relational string operators, which compare UTF-16 code units, so a
name with U+10000 sorted before one with U+FFFD. Use the shared
compareUnicodeCodePoints instead and remove compareProjectionStrings,
whose documented code point contract it did not meet.

Fixes #3177
2026-10-14 16:35:00 +08:00
min eunyoung 535c8ef303 docs(readme): add the Linux ps prerequisite to the Korean README (#3270) 2026-10-14 16:27:54 +08:00
Kaoru0822andClaude Opus 5.5 e5828596c5 fix(artifacts): inventory universal slices REA cannot structurally index (#3283)
* fix(artifacts): inventory universal slices REA cannot structurally index

inspect_artifact and extract_artifact rejected a valid universal Mach-O
when one slice was outside the structural index, such as XLD's
big-endian PowerPC slice. The whole inventory failed with "Artifact is
invalid or has changed. Get a fresh copy and try again."

When lipo reports every slice's range, the slices are now inventoried
from those ranges with a limitation naming the unsupported structure,
as the reader already does for files without structural facts. When
lipo omits a range, the failure is reported as unavailable support
rather than a corrupt artifact.

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

* fix(artifacts): validate every FAT slice past an unsupported one

readFatSlices returned at the first unsupported slice, so a later
malformed slice went unchecked: with lipo's ranges now inventoried for
unsupported structure, a malformed little-endian slice after a
big-endian one was accepted while the reverse order was rejected.
Unsupported slices are now remembered and the remaining slices are
still validated, so malformed structure wins in either order.

Adds portable fake-lipo coverage for complete ranges, missing ranges
(unavailable) and malformed slices on both sides of a big-endian slice.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:22:45 -07:00
Kaoru0822andClaude Opus 5.5 04345767a8 perf(artifacts): run the asset catalog and Interface Builder bundle walks together (#3285)
* perf(artifacts): run the asset catalog and Interface Builder bundle walks together

inspect_asset_catalog walked the app bundle for Assets.car files and then
walked it again for the Interface Builder graph that supplies resource
keys. The analyses are independent, so start the Interface Builder one
first and let a catalog failure cancel it and win, as the sequential
order did. Keynote 4.20 s -> 2.89 s with identical output.

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

* test(artifacts): hold asset catalog and Interface Builder work open to pin overlap

Keep the Interface Builder stub pending until catalog inspection starts,
and keep catalog inspection pending while asserting an early Interface
Builder failure has not escaped.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:22:27 -07:00
ca7450beff test(perf): evaluate larger and non-HTTP packet workflows (#3232)
Add reproducible larger/non-HTTP packet cost measurements, an independent header oracle, exact page validation and separate factual/plan grading. Document caller-selected controls and retain negative/mixed model results. Production defaults and APIs remain unchanged.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-14 16:18:22 +08:00
morluto cd88163e42 fix(cli): show refusals for blocked setup dry runs
Render a terminal dry-run plan only after setup admission succeeds. Blocked skill or registration plans use the existing remediation output. Reuse one bounded PTY helper for success and refusal workflows; verify no linked target or client configuration changes.

Follow-up-to: #3108
Validation: public CLI PTY tests and RPC owner controls pass (77 tests); build and static checks pass.
2026-10-14 16:10:47 +08:00
morluto ccd748635c test(mcp): retain uncertainty for empty capped IDA reference pages
Exercise the empty-but-truncated reference page through trace_feature, preserving unknown coverage without fabricating a reference row.

Fixes #2558.
2026-10-14 16:10:47 +08:00
morlutoandVaishnavi220506 12b3cdb804 fix(javascript): preserve final RPC property and source-body ownership
Normalize exact template and bigint keys at the shared AST boundary. Keep overwritten GraphQL body literals owned by their source object so they cannot resurface as standalone operations.

Reviewed-PR: #3244
Co-authored-by: Vaishnavi220506 <vaishnaviharish2006@gmail.com>
2026-10-14 16:10:46 +08:00
morlutoandHussnain Ahmad eef4a0bfb4 fix(javascript): derive loader references from lexical binding ownership
Use shared lexical bindings and recovered bundler identities for require and chunk references. Preserve uncertain writes as unresolved evidence; remove name-only loader aliases and repeated definition scans.

Reviewed-PR: #2989
Co-authored-by: Hussnain Ahmad <ha7886899@gmail.com>
2026-10-14 16:10:46 +08:00
morlutoandbalkan16 f2f83934d2 fix(javascript): preserve resource error reasons across worker frames
Serialize the typed resource reason once and let the application own path formatting, retaining partial static observations and reported limits.

Reviewed-PR: #3039
Co-authored-by: balkan16 <57933796+balkan16@users.noreply.github.com>
2026-10-14 16:10:46 +08:00
morlutoandwunianze666-netizen f14c2d4df3 fix(browser): retain Playwright IPv6 capture representations
Adapt observed Playwright 1.58.2 bracketed IPv6 addresses only in the disposable HAR validation copy. Preserve the producer spelling and per-entry limitation through public CLI and MCP workflows, retaining existing safe-number admission policy.

Reviewed-PR: #3161
Co-authored-by: wunianze666-netizen <wunianze666@gmail.com>
2026-10-14 16:10:46 +08:00
morlutoandLcokHcak 56128a8424 fix(frida): preserve attach targets and reconcile early detachment
Treat process lookup as optional display metadata, attach to the caller-selected PID, and reconcile already-detached sessions after subscribing to events. Tests observe the actual attach target and the detach race.

Reviewed-PR: #3099
Co-authored-by: LcokHcak <268763621+LcokHcak@users.noreply.github.com>
2026-10-14 16:10:46 +08:00
morluto 5b3400d5d9 fix(pwntools): preserve actionable provider admission errors
Carry the concrete configuration and admission reason through the typed provider-selection error to CLI callers.

Fixes #3055.
Reviewed-PR: #3087
Co-authored-by: 1052326311 <65798732+1052326311@users.noreply.github.com>
2026-10-14 16:10:46 +08:00
morluto 57bb7ec5ca fix(adb): parse successful daemon notices from both output streams
Recognize exact current and legacy ADB startup notices without treating stderr as device inventory or accepting error lookalikes.

Fixes #3051.
Reviewed-PR: #3073
Co-authored-by: Vinícius DS Andrade <95101505+viniciusdsandrade@users.noreply.github.com>
2026-10-14 16:10:46 +08:00
187f113f79 fix(identity)!: preserve invocation uncertainty and advertised catalogs
Capture the current entrypoint once without inventing a path. Preserve unknown registration ownership through setup and doctor, refuse unsafe registration plans, and retain exact pinned-launcher readiness checks. Hash the same full or compact schema projection advertised by each MCP server and use that identity in doctor comparisons.

BREAKING CHANGE: server.command_path may be null when the invocation path is unavailable. Registration and setup statuses, and doctor classifications, may report unknown.

Reviewed-PR: #3230, #3015
Co-authored-by: qorvr <330186599+qorvr@users.noreply.github.com>
Co-authored-by: Hemang Varshney <218273197+hemv-857@users.noreply.github.com>
2026-10-14 16:10:46 +08:00
morlutoandqorvr dd1c879580 fix(windows): end output streams before releasing owned handles
Settle local streams even when native handle release fails, retaining handle ownership and retry semantics until release succeeds.

Reviewed-PR: #3230
Co-authored-by: qorvr <330186599+qorvr@users.noreply.github.com>
2026-10-14 16:10:46 +08:00
morlutoandqorvr c482ddd6ad fix(mcp): release optional providers when transport startup fails
Close the browser scenario owner alongside session, Android and servers when transport startup throws. Preserve cleanup failures in the existing diagnostics.

Reviewed-PR: #3230
Co-authored-by: qorvr <330186599+qorvr@users.noreply.github.com>
2026-10-14 16:10:46 +08:00
morluto 69b8509602 docs(contributing): strengthen issue evidence guidance (#3287) 2026-10-14 16:07:32 +08:00
cf6ea09711 fix(setup): refuse linked skill installation directories (#3108)
Validate final selected skill roots and references before preview, approval,
and client/Hopper changes, with a repeated installation-boundary check.
Preserve interactive unselected-client choices and registration-only setup.

Share typed directory/read observations across setup, doctor, and update;
report unmanaged paths and unknown reads with actionable paths/reasons.
Retain uninstall child outcomes and ownership manifests when cleanup remains.

Shared single-pass inspection retains all unreadable paths after stale or
missing files and keeps observed metadata, without duplicate manifest reads.
Absent installed files do not depend on generated bundled bytes.

Validation: 217 related tests, 41 final CLI/MCP and filesystem checks, and all
7,446 source tests in 508 files pass (eight platform/condition skips).
Static/docs checks pass. Normal four-call
install/rerun/repair/uninstall tasks preserve results and output token counts.
No native Windows follow-up or arbitrary concurrent swap containment claim.

Fixes #3057.

Co-authored-by: geekwolf <3125615+geekwolf@users.noreply.github.com>
Co-authored-by: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: akushonkamen <74654766+akushonkamen@users.noreply.github.com>
2026-10-14 16:00:00 +08:00
morluto 90d4098a01 docs(contributing): require human ownership and review readiness (#3286)
* docs(contributing): require human ownership and review readiness

* docs(contributing): strengthen the human declaration instruction
2026-10-14 15:59:58 +08:00
Kaoru0822andClaude Opus 5.5 bdacd4140e fix(sqlite): keep a retained worker owner after a failed worker exit (#3242)
* fix: abort invalid JSON parses and keep retained worker owners

Main (7375ab6e3) fails four boundary tests from its latest batch.

- f79fd59e9 moved JSON value validation into a non-aborting superRefine, so
  authenticated Evidence still computed its recursive identity digest over
  a 10,000-level `parameters` value and overflowed the stack. The process
  comparison CLI then reported "Maximum call stack size exceeded" instead of
  the depth limit. Abort the parse on a JSON validation issue so enclosing
  refinements never walk invalid data.
- runOwnedCommand retains an incomplete cleanup through retainCleanup and
  then rethrows the command failure without a cleanupOwner. The EVM and SQLite
  providers replaced their retained owner with that undefined value, so an
  output-limit failure with unresolved cleanup lost cleanupIncomplete and its
  owner. Keep the retained owner unless the failure carries one.
- The EVM malformed-reply test expected a nonexistent `output_failure` code;
  AnalysisOutputError projects to `unreadable_output`. The pcap capinfos stub
  prints its report with a trailing newline, which retained output preserves.

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

* test(sqlite): keep a retained worker owner after a failed worker exit

Cover the path the existing worker-cleanup test bypasses: a real owned worker
exits nonzero, runOwnedCommand retains its incomplete cleanup and rethrows the
failure without a cleanupOwner. The provider must still report
cleanupIncomplete and block a new worker until the retained cleanup succeeds.
Fails on the previous owner assignment.

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

* fix(evm): retain the compact worker failure beside retained cleanup

When the worker command fails and its cleanup is retained, the cleanup error
now reports the primary failure in the same compact form as an unretained
command cleanup failure: failure kind, message, exit code and signal. Its
output is already retained once as captured_output, so the previous error no
longer repeats up to a megabyte of diagnostics, and failure_kind stays at the
top level that the provider test reads. Both paths share
ownedCommandPreviousError.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:56:22 -07:00
Kaoru0822andClaude Opus 5.5 307af044bc perf(artifacts): overlap bundle magic reads while enumerating dylib roots (#3248)
* perf(artifacts): overlap bundle magic reads while enumerating dylib roots

trace_dylib_resolution opened every file of an app bundle one at a time
to read its Mach-O magic (Keynote: 35,971 files, 23 Mach-O images).
Keep up to eight reads in flight while the directory walk continues,
settling them in walk order so classification and failure precedence
match the sequential scan. Keynote 4.20 s -> 2.78 s.

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

* test(artifacts): pin ordered-read failure precedence and cleanup

Extract the overlapped magic-read queue into consumeWithOrderedReads and
test its ordering, concurrency bound, failure precedence, enumeration
close and read settlement with controlled promises.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:40:59 -07:00
min eunyoung 9385060f9c fix(website): correct the Korean skip-link label (#3280) 2026-10-14 15:37:59 +08:00
min eunyoung 1c296f11a1 fix(website): correct Korean FAQ translations (#3276) 2026-10-14 15:37:49 +08:00
YsopwnableandKaoru0822 fb752d2293 test(domain): keep rejected Evidence payloads inert (#3260)
Exercise actual Evidence authentication with rejected accessors and proxies in parameters, raw_result and normalized_result. Require zero caller-code reads and preserve the original rejection path without a semantic-ID error.

Closes #3251.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-14 15:34:35 +08:00
morlutoandYog D 8dde6370d0 fix(deps): update the development-only qs serializer
Scope qs 6.16.0 to typed-rest-client under mutation tooling, addressing the affected serializer advisories without changing runtime dependencies. Regenerate the locked dependency inventory; installed dependency tree and unchanged runtime fingerprint verified.

Fixes #3114

Co-authored-by: Yog D <yogd7657@gmail.com>
2026-10-14 15:20:10 +08:00
morlutoandwxggzz 102c5a3ae5 test(mcp): verify availability after cancelled session restoration
Exercise the production session and MCP SDK across provider failure, cancelled target switching, restored run identity, cleared stale availability and successful retry.

Co-authored-by: wxggzz <weixiegen0506@gmail.com>
2026-10-14 15:20:10 +08:00
morlutoandAliciawque 11d38cbc00 test(mcp): verify retained Interface Builder limits
Verify retained hierarchy limits and summary identity through the production artifact-only composition and real MCP SDK, alongside compiled CLI controls. Deep-provider selection is a separate lane.

Co-authored-by: Aliciawque <166712943+Aliciawque@users.noreply.github.com>
2026-10-14 15:20:09 +08:00
7777abf473 fix(artifacts): preserve ZIP member identity and filename semantics
Retain duplicate archive occurrences with validated ordinal identities across pages; qualify ambiguous comparison, parent and source-map relationships instead of overwriting entries. Preserve exact integrity-contradiction ownership. Decode unflagged names as CP437 while honoring UTF-8 and valid Unicode Path overrides.

Fixes #3152

Fixes #3237

Co-authored-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

Co-authored-by: hhang <169027046+xu91102@users.noreply.github.com>

Co-authored-by: gongfpp <1360187968@qq.com>
2026-10-14 15:20:09 +08:00
morlutoandShuaixinHuang 27311f88d4 fix(browser): preserve capture uncertainty and frame ancestry
Parse required resource rows and retain valid siblings. Missing collections and malformed child subtrees keep unknown omission counts. Compare complete frame forests without capture-local IDs, and never infer aggregate DOM/metadata changes from omissions.

Co-authored-by: ShuaixinHuang <1836656514@qq.com>
2026-10-14 15:20:08 +08:00
morlutoandclaudetrail2026-droid eb00112a3f test(javascript): exercise candidate endpoints through the worker
Verify the existing exact candidate-endpoint schema through a completed compiled CLI workflow instead of adding a duplicate worker-only assertion.

Fixes #3128

Co-authored-by: claudetrail2026-droid <313336388+claudetrail2026-droid@users.noreply.github.com>
2026-10-14 15:20:08 +08:00
morlutoandAlex Desir 76fa5304a8 refactor(javascript): share computed-key callable ownership
Give calls, captures, object operations, promises and effects one callable-owner tracker. Suspend a method while its computed key is evaluated in the enclosing callable; remove the capture-only downstream repair.

Co-authored-by: Alex Desir <adesir089@gmail.com>
2026-10-14 15:20:07 +08:00
morlutoandTo3akaRin 6396851958 fix(javascript): retain directive values in function fingerprints
Retain directive literal values, including escaped strict-mode lookalikes, in semantic function fingerprints. Verify normalized duplicates and distinct directives through CLI/MCP.

Co-authored-by: To3akaRin <131595336+To3akaRin@users.noreply.github.com>
2026-10-14 15:20:05 +08:00
morlutoandVaishnavi220506 b8eea783da fix(javascript): honor final RPC object properties
Resolve final object writes once at the RPC endpoint producer. Unknown computed writes and spreads remain barriers; overridden properties do not invent endpoints. Source, CLI and stdio MCP regressions cover the behavior.

Co-authored-by: Vaishnavi220506 <vaishnaviharish2006@gmail.com>
2026-10-14 15:20:05 +08:00
morlutoand吴佳浩 9d00d7d7fa fix(native): retain unclassified accepted Swift manglings
Use the shared Swift prefix owner for admission while preserving legacy classified spellings. Keep unresolved symbols and aliases without letting them override decoded category votes.

Co-authored-by: 吴佳浩 <wujiahao@supremind.com>
2026-10-14 15:20:04 +08:00
morlutoandarlandev 74afb04f32 fix(native): decode the Objective-C word-alignment sentinel
Interpret the 64-bit ivar word-alignment sentinel as eight bytes. Preserve ordinary exponents and unknown invalid encodings; source fixtures only.

Fixes #3186

Co-authored-by: arlandev <arlanpower69@gmail.com>
2026-10-14 15:20:04 +08:00
7df85f16a0 fix(cli): align advertised analysis contracts with runtime
Advertise the nonnegative outline-depth bound after safe-range conversion, verify it against the runtime parser and Ajv 2020, and describe every retained view family and both supported recorded-core architectures. Reuse the existing CLI schema fixture instead of three duplicate capture helpers.

Fixes #3211

Fixes #3194

Fixes #3167

Co-authored-by: Anankequ <2983852214@qq.com>

Co-authored-by: gongfpp <1360187968@qq.com>
2026-10-14 15:20:03 +08:00
morlutoandN0zoM1z0 4c401458da fix(ci): track imported compiler inputs and package scopes
Include imported declarations, nested Node package scopes and authored documentation helpers in compiler task ownership. Schedule static checks for documentation package scopes. Three real cold/warm compiler invalidation cases passed under bounded Flameox captures; the complete matrix remains CI verification.

Fixes #3164

Co-authored-by: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com>
2026-10-14 15:20:03 +08:00
morlutoandSaim Shafique ada133c256 fix(reference): retain unknown kind for unsupported source entries
Keep FIFO, socket and device entries unknown instead of projecting unsupported reader entries as ordinary files. Preserve real file read-failure identity.

Fixes #3218

Co-authored-by: Saim Shafique <msaimshafique4664@gmail.com>
2026-10-14 15:20:02 +08:00
morlutoandSaim Shafique b57a054d5e fix(mcp): cover prompt objectives in catalog identity
Digest the delivered guided-prompt objective alongside metadata and steps. Objective-only changes update prompt and combined identity without changing tools.

Fixes #3222

Co-authored-by: Saim Shafique <msaimshafique4664@gmail.com>
2026-10-14 15:20:02 +08:00
359 changed files with 104157 additions and 25882 deletions

No files matched your search

+3 -3
View File
@@ -68,9 +68,9 @@ closing-issue reporters. Grouping preserves all those credits in one line.
When Highlights, detailed entries or migrations reference the same work,
put its thanks in one of those entries, not all of them. Include issue links.
Direct-commit bullets carry the commit link and known human credit.
Exclude GitHub bots and known agent accounts. REA's observed Anthropic and
Cursor co-author trailers resolve to `@claude` and `@cursoragent` with GitHub
type `User`; the credit policy excludes them while retaining their metadata.
Exclude GitHub bots and known agent accounts. REA's observed Anthropic, Cursor
and Codex co-author trailers resolve to `@claude`, `@cursoragent` and `@codex`
with GitHub type `User`; the credit policy excludes them while retaining their metadata.
At the REA maintainer's request, omit `@morluto` self-thanks while preserving
authorship in the inventory. Other human contributors remain eligible.
Use explicit GitHub links for cross-repository references to avoid ambiguous
+23 -13
View File
@@ -1,5 +1,5 @@
name: Bug report
description: Report a reproducible problem in REA
description: Report a problem in REA with observations or source evidence
title: "[Bug] "
labels:
- bug
@@ -8,8 +8,12 @@ body:
- type: markdown
attributes:
value: |
Please report one primary behavior per issue. Keep observed behavior
separate from suspected causes, and remove credentials and private paths.
Report one primary behavior per issue. Keep answers concise and grounded
in available evidence. Distinguish observed behavior, source-derived
reasoning, and suspected causes; do not claim a reproduction you did not run.
If a narrative answer is not known, write `NOT_ENOUGH_INFO` and describe
what is missing. Intermittent failures and clearly labeled source-only
reports are welcome. Do not include credentials or sensitive target data.
- type: checkboxes
id: before-submitting
@@ -42,7 +46,7 @@ body:
id: observed-behavior
attributes:
label: Observed behavior
description: What happened? Include the command, MCP tool, provider, target type, and conditions that matter.
description: Describe the actual result, including the exact command or MCP request, relevant provider/target, and supporting evidence below. Label source-only findings instead of claiming execution.
validations:
required: true
@@ -50,7 +54,7 @@ body:
id: reproduction
attributes:
label: Reproduction
description: Give the smallest realistic command, configuration, input, or event sequence that demonstrates the problem.
description: Give the shortest steps you actually ran, with the relevant input or configuration. State how often they reproduced the problem when known. If reproduction is unavailable, say so and provide the observations or source reference you have.
placeholder: |
1. Configure or start ...
2. Run ...
@@ -62,7 +66,7 @@ body:
id: expected-behavior
attributes:
label: Expected behavior
description: What result, contract, invariant, recovery action, or state transition should have occurred?
description: State the expected result, contract, or recovery action. Cite documentation, source, or a known-good version when available; mark an unverified expectation or unknown reference clearly.
validations:
required: true
@@ -70,7 +74,7 @@ body:
id: impact-scope
attributes:
label: Impact and scope
description: What is affected, what is not affected, and does this affect evidence integrity, process isolation, compatibility, or recovery?
description: Describe observed impact and frequency, including effects on evidence integrity, process isolation, compatibility, or recovery when known. Leave untested scope and severity unknown rather than guessing.
validations:
required: true
@@ -79,7 +83,7 @@ body:
attributes:
label: REA version
description: Use `rea --version` or provide the source revision.
placeholder: 3.1.0 or git revision
placeholder: Exact rea --version output or source commit SHA
validations:
required: true
@@ -116,7 +120,7 @@ body:
id: failure-stage
attributes:
label: Earliest failure stage
description: Select where the incorrect behavior first becomes observable.
description: Select where the incorrect behavior first becomes observable. Choose Unknown if this has not been established.
options:
- Request parsing or validation
- Setup or provider admission
@@ -133,8 +137,14 @@ body:
id: relevant-evidence
attributes:
label: Relevant output or evidence
description: Paste the smallest useful CLI output, MCP result, error, trace, or evidence excerpt. Redact sensitive values.
render: text
description: Include the smallest useful output, MCP result, error, screenshot, artifact, or exact source reference. Say how it was obtained and which claim it supports. If unavailable, write NOT_ENOUGH_INFO and explain what is missing. Redact credentials and sensitive target data.
placeholder: |
Command or MCP request actually run:
Relevant result, error, or artifact:
For source-only findings, link the file/revision and relevant code:
Missing evidence or remaining uncertainty:
validations:
required: true
- type: textarea
id: typed-outcome
@@ -146,10 +156,10 @@ body:
id: analysis
attributes:
label: Analysis or suspected cause
description: Optional. Clearly mark source-derived reasoning or a hypothesis; do not present an unverified cause as fact.
description: Optional. Link supporting source and distinguish source-derived reasoning from a hypothesis. A root-cause analysis or proposed fix is not required.
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add links, reduced fixtures, screenshots, or a proposed regression test.
description: Add reduced fixtures, screenshots, or related issues. For observed regressions, include the last known good and first known bad versions when available. Label untested checks as proposals.
+1 -1
View File
@@ -41,7 +41,7 @@ body:
id: context-evidence
attributes:
label: Context and evidence
description: Explain the problem or motivation. If you have evidence, distinguish observed behavior from ideas or guesses.
description: Explain the problem or motivation and link relevant evidence when available. Distinguish observed behavior, source-derived reasoning, proposed behavior, and unknowns. A question without measurements or a finished design is welcome.
validations:
required: true
+12 -3
View File
@@ -5,6 +5,13 @@ labels:
- documentation
- triage
body:
- type: markdown
attributes:
value: |
Report one documentation problem with the exact location and the smallest
useful supporting passage, example, or result. Keep answers concise.
If something is unknown, write `NOT_ENOUGH_INFO` and explain what is missing.
- type: checkboxes
id: before-submitting
attributes:
@@ -28,7 +35,7 @@ body:
id: problem
attributes:
label: Problem
description: What is unclear, missing, outdated, or incorrect, and who is affected?
description: Quote or describe the current content, explain what is unclear, missing, outdated, or incorrect, and say how to observe the problem. State who is affected when known.
validations:
required: true
@@ -36,7 +43,7 @@ body:
id: expected-content
attributes:
label: Expected content
description: Describe the accurate behavior or example that should be documented.
description: Describe the accurate behavior or example that should be documented, with a reference when available. Label a proposed clarification or unknown behavior clearly.
validations:
required: true
@@ -44,7 +51,9 @@ body:
id: evidence
attributes:
label: Supporting evidence
description: Add source, command output, provider documentation, or a link that supports the correction.
description: Include an exact passage, source reference, command output, provider documentation, or screenshot supporting the report. For missing or unclear guidance, point to the relevant section and explain the gap. If evidence is unavailable, write NOT_ENOUGH_INFO and explain what is missing.
validations:
required: true
- type: textarea
id: additional-context
+4 -2
View File
@@ -11,6 +11,8 @@ body:
Describe one problem and the outcome you would like. Short answers are
welcome. Choose "Not sure yet" wherever needed; you do not need code, a
finished design, or a complete test plan to propose an idea.
Use concrete examples when available, distinguish current limitations
from proposed behavior, and leave unmeasured impact or unknowns explicit.
Read the [contribution scope](https://github.com/morluto/rea/blob/main/CONTRIBUTING.md#contribution-scope).
Maintainers can help resolve scope and technical questions. Unresolved
@@ -58,7 +60,7 @@ body:
id: use-cases
attributes:
label: Use cases
description: Optional additional examples of applications, targets, or callers. Leave blank if covered above.
description: Optional examples of affected workflows, targets, or callers and their practical impact. Include observed frequency or measurements when available; do not invent them. Leave blank if covered above.
- type: textarea
id: desired-outcome
@@ -138,4 +140,4 @@ body:
id: references
attributes:
label: References
description: Link relevant provider documentation, source, measurements, artifacts, issues, or discussions.
description: Optional supporting examples, provider documentation, source, measurements, artifacts, issues, or prior art. Distinguish observed results from expected benefits; an idea without evidence is welcome.
+24 -3
View File
@@ -24,7 +24,7 @@ Unresolved scope, authority, side effects, or required verification block
integration. Link the required maintainer scope decision for new providers or
capabilities involving target execution, injection, mutation, or protection
recovery; ordinary fixes within existing boundaries may state "not applicable".
Draft PRs may leave the scope decision pending and the final review checklist
Draft PRs may leave the scope decision pending and the readiness and merge checklists
unchecked. Maintainers can help resolve open questions.
## Contract and boundary impact
@@ -55,7 +55,10 @@ Unchecked items do not establish absence; leave uncertainties visible.
## Evidence and regression coverage
<!-- State whether evidence is executed, source-derived, or proposed. For fixes,
explain how the regression would fail on the affected base revision when practical. -->
show the original failure and explain how the regression would fail on the affected
base revision when practical. Include the most useful proof inline; link long logs
or artifacts, or put them in optional details. Documentation-only changes can use
source, link, or rendered-document checks. -->
- Tests added or updated:
- Base reproduction or other evidence:
@@ -71,7 +74,8 @@ For evidence-bearing changes:
## Validation performed
<!-- List only commands that actually ran, with observed results. Include the
relevant real-provider or platform workflow, or explain why it was unavailable. -->
relevant real-provider or platform workflow, or explain why it was unavailable.
Keep these results current after changes; a command without its result is incomplete. -->
- `command` — result
@@ -90,6 +94,23 @@ limits, security/privacy/containment implications, and meaningful proof gaps. --
- Package or release metadata impact:
- Security, privacy, process, or containment review:
## Author readiness
<!-- The human owner completes these declarations before requesting review.
The PR author is the owner by default. A different owner must explicitly accept
ownership on this PR.
AI agents must leave these declarations to a human to dictate to them or there could be catastrophic consequences for many users of REA.
Draft PRs may leave them unchecked. These declarations record initial readiness
when opening the PR for review; later pushes do not require a new human sign-off.
Keep the description and validation evidence current as the work changes.
See CONTRIBUTING.md#pull-request-ownership-and-readiness. -->
Human owner: <!-- @GitHub-handle -->
- [ ] I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work.
- [ ] I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps.
- [ ] I declare this PR ready for initial maintainer review.
## Before merge
<!-- Draft PRs may leave pending items unchecked. Resolve material scope and
+16 -3
View File
@@ -250,7 +250,7 @@ jobs:
retention-days: 7
reverse-tools-quality:
name: Reverse tools coverage and mutation
name: Reverse tools quality
needs: [changes, static]
if: needs.changes.outputs.reverse_tools == 'true'
runs-on: ubuntu-latest
@@ -266,8 +266,21 @@ jobs:
- run: npm ci
- name: Run reverse tools tests with coverage
run: npm run test:coverage:reverse-tools
- name: Report reverse tools mutation results
run: npm run test:mutation:reverse-tools
- name: Preflight both reverse tools mutation configurations
run: |
node scripts/prepare-reverse-tools-mutation.mjs
npx --no-install stryker run stryker.config.mjs --dryRunOnly
npx --no-install stryker run stryker.semantics.config.mjs --dryRunOnly
- name: Retain available reverse tools diagnostics
if: ${{ !cancelled() }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: reverse-tools-quality-${{ needs.changes.outputs.tier }}-${{ github.run_attempt }}
path: |
coverage/reverse-tools/
reports/mutation/
if-no-files-found: warn
retention-days: 7
build:
name: Build package
+62
View File
@@ -0,0 +1,62 @@
name: Mutation sweep
on:
schedule:
- cron: "47 4 * * *"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: mutation-${{ github.ref }}
cancel-in-progress: false
env:
HUSKY: "0"
jobs:
mutation:
name: Mutation sweep (${{ matrix.suite }})
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
max-parallel: 2
matrix:
include:
- suite: reverse-tools
config: stryker.config.mjs
- suite: reverse-tools-semantics
config: stryker.semantics.config.mjs
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
uses: ./.github/actions/npm-cache
- run: npm ci
- run: npm run test:prepare
- name: Run the complete configuration
env:
MUTATION_CONFIG: ${{ matrix.config }}
run: |
node scripts/prepare-reverse-tools-mutation.mjs
npx --no-install stryker run "${MUTATION_CONFIG}"
- name: Require both reports from the completed sweep
env:
REPORT_BASE: reports/mutation/${{ matrix.suite }}
run: |
test -s "${REPORT_BASE}.json"
test -s "${REPORT_BASE}.html"
- name: Retain available mutation diagnostics
if: ${{ !cancelled() }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: mutation-${{ matrix.suite }}-${{ github.run_attempt }}
path: |
reports/mutation/${{ matrix.suite }}.json
reports/mutation/${{ matrix.suite }}.html
if-no-files-found: warn
retention-days: 7
+51
View File
@@ -0,0 +1,51 @@
name: PR readiness
on:
pull_request_target:
branches: [main, "release/*"]
types:
[
opened,
edited,
synchronize,
reopened,
ready_for_review,
converted_to_draft,
]
workflow_dispatch:
inputs:
pull_request_number:
description: Open PR to recheck
required: true
type: string
permissions: {}
concurrency:
group: pr-readiness-${{ github.event.pull_request.number || inputs.pull_request_number }}
cancel-in-progress: true
jobs:
readiness:
name: Validate author declaration
runs-on: ubuntu-24.04
timeout-minutes: 3
permissions:
contents: read
pull-requests: read
statuses: write
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false
- name: Check initial readiness
uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9
with:
script: |
const { pathToFileURL } = require("node:url");
const entry = pathToFileURL(
`${process.env.GITHUB_WORKSPACE}/scripts/ci/pr-readiness.mjs`,
);
const { runReadinessCheck } = await import(entry.href);
await runReadinessCheck({ github, context, core });
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "6.4.0"
".": "6.5.0"
}
+31
View File
@@ -78,6 +78,37 @@ For test changes or provider claims, use [docs/testing.md](docs/testing.md). Pre
Use Conventional Commit subjects and PR titles. Mark breaking changes with `!` or a `BREAKING CHANGE:` footer. Describe behavior changes, relevant validation, and real-provider verification scope.
## Issue Reports
Follow [issue reporting guidance](CONTRIBUTING.md#reporting-issues) and the matching
issue form. Keep reports concise and cite the smallest useful evidence.
Distinguish observed results, source-derived reasoning, hypotheses, and proposed
checks. Never claim runtime reproduction or measurements you did not perform.
Use `NOT_ENOUGH_INFO` with an explanation for missing narrative answers or evidence.
Incomplete reports, source-only findings, and early feature or design ideas are
welcome when their limitations are explicit; a completed diagnosis or fix is not
required to open an issue.
## Pull Request Readiness
Follow [human ownership and readiness](CONTRIBUTING.md#pull-request-ownership-and-readiness)
when preparing a PR. AI-assisted contributions are welcome; a human must review
and own the final diff, check the validation evidence and its limitations, and
explicitly declare the PR ready for initial maintainer review.
Keep the PR body concise and current, with the concrete problem, observable
outcome, approach, and evidence. Report only checks that actually ran as executed;
distinguish source-derived conclusions, proposed checks, and unavailable coverage.
Address actionable automated review findings and explain disagreements.
Leave the template's **Author readiness** declarations unchecked for the human
owner to complete. Never attest to human review on their behalf. Keep the PR in
draft until the human declares the PR initially ready. Later pushes do not
invalidate that declaration or require another human sign-off. Keep the PR body
and affected validation current as work changes. The readiness check validates
the filled declaration; CI and automated review results do not establish that
human review occurred, maintainer scope approval, or merge permission.
<!-- BEGIN:turborepo-agent-rules -->
# This is NOT the Turborepo you know
+113
View File
@@ -1,5 +1,118 @@
# Changelog
## [6.5.0](https://github.com/morluto/rea/compare/rea-agents-6.4.0...rea-agents-6.5.0) (2026-10-13)
Provider prerequisites and recorded verification scope are listed in [the testing guide](docs/testing.md).
### Highlights
- Analyze selected JavaScript files and recover cross-bundle relationships without expanding the selected source scope.
- Read compact summaries and page retained Evidence across binary, managed, Android, Dart, WASM and network workflows.
- Inspect SQLite snapshots, packet and signal recordings, WebAssembly, Rust dependency metadata and Electron fuses offline.
- Keep completed Evidence and partial observations available when analysis or cleanup fails.
### Changes
- **JavaScript source selection:** analyze exact application paths without expanding imports into the selected scope. Unselected files retain identity and explicit unknown coverage; summary delivery remains separate from source selection. ([#1944](https://github.com/morluto/rea/pull/1944), [issue morluto/rea#1848](https://github.com/morluto/rea/issues/1848)). Thanks @kaoru0822-kitauji, @N0zoM1z0.
- **Code-split recovery:** recover related bundles from multiple selected inputs, retaining per-file provenance and downstream cross-chunk imports. Recorded CLI/MCP coverage is Linux x64 with Wakaru 1.14.0 and source-owned esbuild/webpack fixtures; other hosts, releases and arbitrary application equivalence remain unverified. ([#1686](https://github.com/morluto/rea/pull/1686), [issue morluto/rea#1605](https://github.com/morluto/rea/issues/1605)). Thanks @JingHao-Leon, @pionxzh.
- **Package imports:** resolve exact package import aliases using the selected loader and package ancestry, rather than guessing sibling files. ([#1862](https://github.com/morluto/rea/pull/1862), [issue morluto/rea#1838](https://github.com/morluto/rea/issues/1838)). Thanks @AWUQAQ, @mvanhorn.
- **Retained analysis views:** opt into compact inventory/WASM, managed metadata, Android manifest, Dart AOT, Mach-O and EVM summaries, then page retained Evidence without rerunning analysis. Summary delivery preserves the complete recorded result and its original coverage. ([#2122](https://github.com/morluto/rea/pull/2122), [#2193](https://github.com/morluto/rea/pull/2193), [#2413](https://github.com/morluto/rea/pull/2413), [#2438](https://github.com/morluto/rea/pull/2438), [#2455](https://github.com/morluto/rea/pull/2455), [#2634](https://github.com/morluto/rea/pull/2634), [#2308](https://github.com/morluto/rea/pull/2308), [9a18251](https://github.com/morluto/rea/commit/9a18251426f891e6a24856b715ef8002ab8b9610)). Thanks @kaoru0822-kitauji, @N0zoM1z0.
- **Managed assemblies:** Select declaring types when inspecting members, decode inspected `ldstr` literals with independent coverage, and page retained member summaries. Call and field targets expose `target_declaring_type` and `field_declaring_type`, including supported generic method targets; unresolved metadata remains unknown. ([#1706](https://github.com/morluto/rea/pull/1706), [#2125](https://github.com/morluto/rea/pull/2125), [issue morluto/rea#2121](https://github.com/morluto/rea/issues/2121), [#2162](https://github.com/morluto/rea/pull/2162), [issue morluto/rea#2158](https://github.com/morluto/rea/issues/2158), [#3061](https://github.com/morluto/rea/pull/3061), [issue morluto/rea#3010](https://github.com/morluto/rea/issues/3010), [#2163](https://github.com/morluto/rea/pull/2163), [issue morluto/rea#2160](https://github.com/morluto/rea/issues/2160)). Thanks @kaoru0822-kitauji, @N0zoM1z0, @Nelglor, @skoodster.
- **Packet captures:** inspect saved packets with bounded TShark output, compact capture summaries and retained pages; discover installed protocol/field definitions. Recorded real-engine coverage uses Linux and Wireshark 4.6.6; native Windows/macOS and Bun behavior remain unverified. ([#2318](https://github.com/morluto/rea/pull/2318), [#2340](https://github.com/morluto/rea/pull/2340), [#2496](https://github.com/morluto/rea/pull/2496)). Thanks @3QL, @N0zoM1z0, @Olorinm.
- **Recorded signals:** decode existing recordings and discover installed decoders/options through caller-supplied sigrok-cli 0.7.2. This is Linux-only and requires util-linux `prlimit`; it acquires no hardware samples. ([#2339](https://github.com/morluto/rea/pull/2339), [#2487](https://github.com/morluto/rea/pull/2487)). Thanks @N0zoM1z0, @Olorinm.
- **Web captures:** derive an inferred method/origin/path-template API surface from retained HAR data including observed query-parameter names while retaining unknowns; query values, headers and bodies remain excluded. ([#1891](https://github.com/morluto/rea/pull/1891), [#2982](https://github.com/morluto/rea/pull/2982), [issue morluto/rea#2913](https://github.com/morluto/rea/issues/2913)). Thanks @mvanhorn, @wunianze666-netizen, @zhangguiping-xydt.
- **QUIC captures:** inspect retained aioquic 1.5.0 JSON qlog 0.3 objects and page traces/streams/observations. Other qlog layouts remain unsupported; overlap and loss declarations do not prove retransmission or network loss. ([#2277](https://github.com/morluto/rea/pull/2277)). Thanks @LuisTaffelli, @N0zoM1z0.
- **Fiddler captures:** inspect native SAZ archives with retained HTTP provenance and bounded decoding. Recorded producer coverage is the pinned public encoding corpus; current Fiddler Everywhere variants and Bun-native execution remain unverified. ([#2409](https://github.com/morluto/rea/pull/2409)). Thanks @N0zoM1z0, @zahhak.
- **SQLite snapshots:** inspect an explicitly selected offline database with table/column selection and bounded Evidence. This operation requires Node 24.x >=24.15 or 26+ with `DatabaseSync.limits`; supported Node 22 and earlier 24 runtimes cannot use it. A live database/WAL copy does not establish an atomic snapshot. ([#2271](https://github.com/morluto/rea/pull/2271)). Thanks @N0zoM1z0, @Y0UW1SH.
- **WASM artifacts:** Inspect local modules with bring-your-own WABT 1.0.42 without executing them. Compare imports/exports from two complete core-module inspection Evidence records without requiring an active WABT installation; unsupported signatures and duplicate identities remain unknown, and comparisons do not establish runtime compatibility. Recorded real-tool inspection coverage is macOS arm64. ([#1533](https://github.com/morluto/rea/pull/1533), [issue morluto/rea#884](https://github.com/morluto/rea/issues/884), [#2464](https://github.com/morluto/rea/pull/2464), [issue morluto/rea#2356](https://github.com/morluto/rea/issues/2356)). Thanks @matbrgz, @N0zoM1z0, @thangldw.
- **Go binaries:** inspect embedded build metadata, including file-backed unmapped Mach-O debug segments. ([#1363](https://github.com/morluto/rea/pull/1363), [7fb097d](https://github.com/morluto/rea/commit/7fb097d72c9931dd5dfae481ad2b9d3bb6029f15)). Thanks @Y0UW1SH.
- **Flutter APKs:** identify snapshot/engine builds and inspect AOT structure without an external engine. Recorded real APK coverage uses the documented Flutter Gallery fixture; macOS/Windows and complete pool semantics remain unverified, and pool coverage stays partial. ([#1582](https://github.com/morluto/rea/pull/1582)). Thanks @3QL.
- **HarmonyOS packages:** inventory HAP/HSP/App Pack structures without executing code. The recorded real workflow covers one pinned HAP through repository-built CLI/MCP; installed-package and real HSP/App Pack coverage remain unverified. ([#1447](https://github.com/morluto/rea/pull/1447)). Thanks @BaaRai.
- **Ghidra raw inputs and references:** compatible Linux/macOS Ghidra profiles accept caller-selected raw architecture/entry information and expose typed incoming references at exact addresses. Windows Ghidra P0 admits the raw NMOS 6502 profile described below; other raw targets and the incoming-reference facet remain outside Windows P0. ([#1868](https://github.com/morluto/rea/pull/1868), [issue morluto/rea#1836](https://github.com/morluto/rea/issues/1836), [#1715](https://github.com/morluto/rea/pull/1715), [issue morluto/rea#1245](https://github.com/morluto/rea/issues/1245)). Thanks @bugstan, @casesolved-co-uk, @mvanhorn, @N0zoM1z0.
- **Ghidra MIPS:** Add bounded MIPS32r2/o32 relocatable objects, soft-float integer-register storage, admitted MIPS16 shared ELF, and a narrowly admitted recordless legacy MIPS32 executable profile. The latter pins an endian-matched 32-bit language and records inferred o32 as an analysis assumption. Admission follows each documented ABI/flag/relocation boundary. ([#1949](https://github.com/morluto/rea/pull/1949), [issue morluto/rea#1937](https://github.com/morluto/rea/issues/1937), [#1950](https://github.com/morluto/rea/pull/1950), [issue morluto/rea#1938](https://github.com/morluto/rea/issues/1938), [#1991](https://github.com/morluto/rea/pull/1991), [issue morluto/rea#1851](https://github.com/morluto/rea/issues/1851), [#1898](https://github.com/morluto/rea/pull/1898), [issue morluto/rea#1620](https://github.com/morluto/rea/issues/1620)). Thanks @AurelioB, @BaaRai, @bqw371, @kaoru0822-kitauji, @N0zoM1z0.
- **Ghidra RISC-V:** admit little-endian RV32/RV64 `ET_EXEC` ELF declaring the supported double-float ABI. Relocatable/shared, big-endian and other ABI profiles remain refused; see [the profile and recorded lane](docs/ghidra-riscv.md). ([#2151](https://github.com/morluto/rea/pull/2151)). Thanks @NIU-123370.
- **PSP static ELF profile:** the Ghidra profile is Linux x64, bounded little-endian ELF32 `ET_EXEC` with the documented PSP/ABI declarations and a compatible caller-installed Allegrex extension. PRX, shared/relocatable images, PBP/ISO/CSO and runtime emulation are outside scope. Historical extension experiments are separate from public-provider verification; real public Ghidra PSP coverage remains unverified. ([#1599](https://github.com/morluto/rea/pull/1599)). Thanks @jaelliot.
- **Ghidra analysis settings:** apply validated analyzer overrides before analysis while retaining the selected analysis profile. ([#1913](https://github.com/morluto/rea/pull/1913)). Thanks @akushonkamen, @kaoru0822-kitauji, @N0zoM1z0.
- **Attached native IDA MCP:** add an optional native bridge alongside existing legacy/headless profiles. Recorded native runs use Linux IDA Free 9.4 with a navigation correction and Windows IDA Pro 9.3 with a loopback-listener change; stock native switching, native macOS and Windows IDA Free remain unverified. Attached databases remain read-only and are never saved or closed. ([#1544](https://github.com/morluto/rea/pull/1544)). Thanks @imissapixel, @N0zoM1z0.
- **Frida instrumentation:** add explicit local/remote target selection, caller-supplied scripts and retained messages, with script-read/input diagnostics and bounded message eviction. Recorded real coverage is the Linux local Node marker workflow; macOS, Windows and remote devices/servers remain unverified. Instrumentation executes foreign code and is destructive, not read-only. ([#1359](https://github.com/morluto/rea/pull/1359), [#2211](https://github.com/morluto/rea/pull/2211), [issue morluto/rea#2209](https://github.com/morluto/rea/issues/2209), [#2217](https://github.com/morluto/rea/pull/2217), [issue morluto/rea#2213](https://github.com/morluto/rea/issues/2213), [#2628](https://github.com/morluto/rea/pull/2628)). Thanks @dandgabr, @kaoru0822-kitauji, @N0zoM1z0.
- **Client setup:** register Qoder, Trae, ZCode, DeepSeek Harness, Kimi Code, Factory Droid and WorkBuddy through the existing scoped setup flow. DeepSeek is included from 6.5.0; the historical npm 6.4 check did not cover it. Live Trae and DeepSeek API integration remain unverified. ([#1564](https://github.com/morluto/rea/pull/1564), [#1572](https://github.com/morluto/rea/pull/1572), [#1573](https://github.com/morluto/rea/pull/1573), [#1929](https://github.com/morluto/rea/pull/1929), [issue morluto/rea#1839](https://github.com/morluto/rea/issues/1839), [#2744](https://github.com/morluto/rea/pull/2744), [#2309](https://github.com/morluto/rea/pull/2309), [#3041](https://github.com/morluto/rea/pull/3041), [issue morluto/rea#3040](https://github.com/morluto/rea/issues/3040)). Thanks @0xRafie, @BaaRai, @cainiao1992, @IvenKooLab, @whoishaha, @ysntony.
- **Website:** Add maintained language editions, persistent/system-aware themes, keyboard styling and nested Evidence indentation, plus the reverse-engineering models essay. A simpler home page and beginner journey connect platform prerequisites, client/provider setup and a supplied first investigation. ([#1779](https://github.com/morluto/rea/pull/1779), [issue morluto/rea#1022](https://github.com/morluto/rea/issues/1022), [#1699](https://github.com/morluto/rea/pull/1699), [#1714](https://github.com/morluto/rea/pull/1714), [#1894](https://github.com/morluto/rea/pull/1894), [#2250](https://github.com/morluto/rea/pull/2250), [#2432](https://github.com/morluto/rea/pull/2432), [#2732](https://github.com/morluto/rea/pull/2732), [#3274](https://github.com/morluto/rea/pull/3274)). Thanks @CassieFox23, @kaoru0822-kitauji, @N0zoM1z0.
- **Rust dependency metadata:** Inspect embedded cargo-auditable package/dependency records in ELF, PE and thin Mach-O without executing the file. Missing metadata and inferred compiler hashes retain explicit limitations; universal Mach-O requires a selected thin slice. ([#2456](https://github.com/morluto/rea/pull/2456)). Thanks @mvanhorn.
- **Electron fuses:** Read packaged fuse bytes and per-slice runtime implications without launching or modifying the app. Missing, truncated and unknown wires remain explicit; fuse observations do not prove other Inspector-enablement paths. ([#2399](https://github.com/morluto/rea/pull/2399)). Thanks @kaoru0822-kitauji, @mvanhorn, @N0zoM1z0.
- **Ghidra PowerPC:** Admit bounded ELF32 big-endian `ET_EXEC` PowerPC with an explicit language profile on supported Linux/macOS hosts. PPC64, DOL and other variants remain outside this lane; see [the profile and verification prerequisites](docs/ghidra-ppc.md). ([#1747](https://github.com/morluto/rea/pull/1747)). Thanks @arielfikru.
- **Reference-source summaries:** Use CLI `import-reference-source --detail summary` for compact manifest, count, state and path-outline output. The complete graph remains available and is still the default; hidden outline entries retain separate coverage counts. ([#2768](https://github.com/morluto/rea/pull/2768), [issue morluto/rea#2442](https://github.com/morluto/rea/issues/2442)). Thanks @matbrgz, @To3akaRin.
- **Recorded AArch64 crashes:** Decode 34 ABI register values per thread from supported Linux ELF64 little-endian core notes using the documented caller-provided Python dependencies. These are recorded facts, not proof of current process or library identity. ([#2505](https://github.com/morluto/rea/pull/2505), [issue morluto/rea#2353](https://github.com/morluto/rea/issues/2353)). Thanks @matbrgz, @NIU-123370.
- **Ghidra byte search:** Search initialized analyzed memory using wildcards, bit masks, address bounds and paged results. Unloaded, overlay and uninitialized blocks retain skipped coverage; this is not a raw-file search. ([#2341](https://github.com/morluto/rea/pull/2341)). Thanks @mvanhorn.
- **Raw NMOS 6502 images:** Analyze explicitly selected headerless images with Ghidra, including Windows P0, using a caller-selected entry and source mapping within the 16-bit address space. Cartridge parsing, reset-vector inference and 65C02/65816 are outside this profile. ([#2079](https://github.com/morluto/rea/pull/2079)). Thanks @Zhong-fan.
- **Swift String observations:** Ghidra function dossiers derive supported AArch64 Swift String literals from constant-register pairs at recognized use sites, retaining instruction/register provenance and explicit omissions. This is a bounded derivation, not a complete string inventory. ([#2049](https://github.com/morluto/rea/pull/2049), [issue morluto/rea#2030](https://github.com/morluto/rea/issues/2030)). Thanks @thefolahan.
- **Headless IDA procedure lookup:** Resolve addresses to canonical containing-procedure entries and identify the session’s admitted program document in the modern headless profile. Incomplete surveys remain errors; attached profiles do not advertise this containment capability. ([#2374](https://github.com/morluto/rea/pull/2374), [issue morluto/rea#2335](https://github.com/morluto/rea/issues/2335)). Thanks @nikolay-dementiev.
- **Passive browser attachment:** Six existing passive browser tools accept an explicit-port loopback browser WebSocket endpoint when HTTP target discovery is unavailable. Existing consent and target/origin checks remain required; this does not launch a browser or enable runtime instrumentation. ([#2346](https://github.com/morluto/rea/pull/2346)). Thanks @all-black-493.
- **Android DEX references:** Retain exact method descriptors and individual direct-invoke observations with source method, DEX entry, opcode and code-unit offset. These are static references; unavailable instruction facts remain separate from method-level usage totals. ([#2439](https://github.com/morluto/rea/pull/2439)). Thanks @LcokHcak.
- **Offline EVM metadata:** Retain bounded, lossless facts from plausible terminal Solidity CBOR root-map candidates. Candidates do not prove Solidity origin or source/compiler identity, and linked metadata is not fetched. ([#2790](https://github.com/morluto/rea/pull/2790)). Thanks @matbrgz.
### Fixes
- **JavaScript loader and export identity:** Respect import/require conditions and unavailable package metadata, keep export links separate from writes, and resolve default identifier exports to the value captured at the export statement. ([7e5cffa](https://github.com/morluto/rea/commit/7e5cffa2ae9dfc9afaeca0715110d4dd41c0dcdd), [322506d](https://github.com/morluto/rea/commit/322506d0c0d39d3cd1e2e1c285c2cc23cfa73ee7), [81f7638](https://github.com/morluto/rea/commit/81f763820319a762f003cf95651ed14ce9326a4c), [dae1e55](https://github.com/morluto/rea/commit/dae1e55381f0d6cfee7f2082d5d27b938e55704c)). Thanks @paoValle, @VarunGore36, @xu91102.
- **JavaScript traces:** Retain nested bridge calls and surviving early-return predicates. Honor node-ID match options, match route seeds against source values, and keep ambiguous runtime candidates unknown. ([0351328](https://github.com/morluto/rea/commit/0351328e54ce70ea3f2814482e76b132774aff48), [5031099](https://github.com/morluto/rea/commit/503109995f9dcf8a955585d39a344fc846d2f59e), [44c9d29](https://github.com/morluto/rea/commit/44c9d2924d5b95d7a86595e8d7b7cf4fd4f0f6ac), [5e79d46](https://github.com/morluto/rea/commit/5e79d468e5864204cbe79a7ca2898541cee5e012)). Thanks @jpdefo.
- **Browser comparison coverage:** Compare only known redirect response fields and retain network-selection recovery guidance alongside independent event changes. Missing response facts do not prove equality or a change. ([2b60e09](https://github.com/morluto/rea/commit/2b60e097f281f7708c69f1f7ba9263694547bcc0)).
- **Evidence comparison and ledger input:** Report display-only bundle differences as unchanged while retaining digests, and classify noncanonical bundle records as invalid caller input. ([030c477](https://github.com/morluto/rea/commit/030c47732833c9cedcc31db1309eb3a1ca2c62e8), [3a34ad3](https://github.com/morluto/rea/commit/3a34ad37bdb7b8debba0f6515ba85b80141f2716)).
- **Capture and admission cleanup:** Keep deleted-file observations and original bytes when a parent becomes a file, preserve unrelated I/O failures during cancellation, and require explicit pre-launch proof before releasing an unknown LLDB target owner. LLDB failure controls used simulated boundaries. ([e67c364](https://github.com/morluto/rea/commit/e67c3642c8920a2a9c0e231ee2f16f50b245df31), [2daeff4](https://github.com/morluto/rea/commit/2daeff4b243e1cd137c591ca394e18a1c05c8bbc), [45b6a7f](https://github.com/morluto/rea/commit/45b6a7fe8f51a89951aaa823ffa627f6f5453a33)). Thanks @SII-YDD, @xu91102.
- **Client configuration:** Validate MCP output-schema profiles before setup writes, ignore untrusted Gemini workspace settings before parsing them, and identify installed skills independently of marker-only client roots. ([0930e94](https://github.com/morluto/rea/commit/0930e94e077ab7fb4d695f62785919a8a25c2e21), [aade9bb](https://github.com/morluto/rea/commit/aade9bbcb79aa8504ab6921b25f8ab082aba78e7), [ba33c70](https://github.com/morluto/rea/commit/ba33c706d26b63e43308be50c0249c72cff62cc6)). Thanks @CmsChase, @paoValle, @sx4im.
- **Host tool discovery:** Share macOS Hopper launcher discovery across setup and runtime, preserve explicit paths, and report validated ADB and Apktool availability. ([7582f0d](https://github.com/morluto/rea/commit/7582f0dff29ae0e80c97b885ddd386ac61a92570), [e5deb83](https://github.com/morluto/rea/commit/e5deb832cb09bb524877d0df40f49d5b5bb7dea4)). Thanks @Chessing234, @kaoru0822-kitauji, @viniciusdsandrade.
- **SAZ encoding validation:** Reject invalid XML bytes, unsupported encodings and declarations that contradict the archive bytes, including records outside the selected request. ([1250aae](https://github.com/morluto/rea/commit/1250aaedd8b4d1e471d8a382f76418bde65c05d2)).
- **Managed member matching:** Keep malformed TypeDef list ownership unknown, distinguish constants during structural method matching, and accept legacy small exception-section sizes. ([3f6b575](https://github.com/morluto/rea/commit/3f6b5755b3e699bd9373641acffe0f2c5d2ab094), [ee5cfbe](https://github.com/morluto/rea/commit/ee5cfbe05cd309a88003c711721e67a3bee36554)).
- **Bounded diagnostics:** Bound SDK validation messages and terminal normalization before allocation while retaining diagnostics and explicit unknown final output sizes. ([7fb0a0c](https://github.com/morluto/rea/commit/7fb0a0cc577f7c9033a2674d877d8242b5864556), [1d826f3](https://github.com/morluto/rea/commit/1d826f304f032acfe69dad3ff824b300aacaeb43)). Thanks @CRYPTONIKAV.
- **Windows artifact classification:** identify `.exe`, `.com`, `.bat` and `.cmd` artifacts as launchable host executables. ([#1823](https://github.com/morluto/rea/pull/1823), [#1824](https://github.com/morluto/rea/pull/1824), [issue morluto/rea#1819](https://github.com/morluto/rea/issues/1819)). Thanks @dollarsmoney, @JasonBuildAI, @kaoru0822-kitauji.
- **CLI/MCP delivery:** defer command-specific startup work, bundle runtime entries, retain field/input correction details across output formats and preserve shared optional-provider lifetime. MCP selectors, paging continuation and retained partial-error references remain explicit; session liveness/provider-attribution corrections retain their controlled-provider verification limits. ([#1691](https://github.com/morluto/rea/pull/1691), [issue morluto/rea#1685](https://github.com/morluto/rea/issues/1685), [#1705](https://github.com/morluto/rea/pull/1705), [issue morluto/rea#1695](https://github.com/morluto/rea/issues/1695), [#1755](https://github.com/morluto/rea/pull/1755), [issue morluto/rea#1746](https://github.com/morluto/rea/issues/1746), [#1773](https://github.com/morluto/rea/pull/1773), [issue morluto/rea#1771](https://github.com/morluto/rea/issues/1771), [#1780](https://github.com/morluto/rea/pull/1780), [#1814](https://github.com/morluto/rea/pull/1814), [#1910](https://github.com/morluto/rea/pull/1910), [issue morluto/rea#1849](https://github.com/morluto/rea/issues/1849), [#1996](https://github.com/morluto/rea/pull/1996), [#2140](https://github.com/morluto/rea/pull/2140), [issue morluto/rea#2135](https://github.com/morluto/rea/issues/2135), [#2205](https://github.com/morluto/rea/pull/2205), [issue morluto/rea#2195](https://github.com/morluto/rea/issues/2195), [#2230](https://github.com/morluto/rea/pull/2230), [issue morluto/rea#2180](https://github.com/morluto/rea/issues/2180), [#2294](https://github.com/morluto/rea/pull/2294), [#2482](https://github.com/morluto/rea/pull/2482), [a82d14e](https://github.com/morluto/rea/commit/a82d14e4db4dca989ffa2e7dbe78fd43551da256), [a4dd4e8](https://github.com/morluto/rea/commit/a4dd4e8e8700ce350e1a0c612ebd61b91ca5e585), [48c352b](https://github.com/morluto/rea/commit/48c352b6bfa34c820a530272cd36600158c37b1e), [07b36c1](https://github.com/morluto/rea/commit/07b36c148946f46b99a4ce04e55726e97d0ee03a), [570c40c](https://github.com/morluto/rea/commit/570c40cde55d31e455ef9bc484b7b908658ded58), [8fc2043](https://github.com/morluto/rea/commit/8fc2043eb8bcedbf34dce064f977e455438635d6), [7fc0b5e](https://github.com/morluto/rea/commit/7fc0b5ec9c2532bbf789d7783b7a0cab6fc054d8)). Thanks @369Apps, @chqcjq, @cold-atom, @coygeek, @JackOdegard, @kaoru0822-kitauji, @N0zoM1z0, @sx4im.
- **Evidence import/replay:** import bundles across session transitions while authenticating selected target and file identities, preserve literal keys, metadata/mutation intent and relationship coverage, and honor cancellation before session mutation. Reuse authenticated snapshots and comparisons within their owned lifetime; reject malformed/non-object snapshots and unsupported metadata without disguising uncertainty. ([#1694](https://github.com/morluto/rea/pull/1694), [#1716](https://github.com/morluto/rea/pull/1716), [issue morluto/rea#1680](https://github.com/morluto/rea/issues/1680), [#1721](https://github.com/morluto/rea/pull/1721), [issue morluto/rea#1718](https://github.com/morluto/rea/issues/1718), [#1733](https://github.com/morluto/rea/pull/1733), [issue morluto/rea#1723](https://github.com/morluto/rea/issues/1723), [issue morluto/rea#1724](https://github.com/morluto/rea/issues/1724), [#1776](https://github.com/morluto/rea/pull/1776), [#1873](https://github.com/morluto/rea/pull/1873), [#2001](https://github.com/morluto/rea/pull/2001), [#2013](https://github.com/morluto/rea/pull/2013), [issue morluto/rea#2009](https://github.com/morluto/rea/issues/2009), [#2083](https://github.com/morluto/rea/pull/2083), [#2303](https://github.com/morluto/rea/pull/2303), [#2685](https://github.com/morluto/rea/pull/2685), [issue morluto/rea#2675](https://github.com/morluto/rea/issues/2675), [c002b89](https://github.com/morluto/rea/commit/c002b89e2d8130a3c07e25e5fd604876be18c703), [9c85114](https://github.com/morluto/rea/commit/9c8511409c0706f9979c2b68dd56aaaf9f21e191), [aada0ca](https://github.com/morluto/rea/commit/aada0ca8126bf14c60750a4ccd4a79667770a1e4), [4a4d36f](https://github.com/morluto/rea/commit/4a4d36f881861bbab3c734b0dd459f805766caec), [1c64fe3](https://github.com/morluto/rea/commit/1c64fe3eff48679c6880f42d2f0a4af46686ade7), [6fb3a34](https://github.com/morluto/rea/commit/6fb3a345fe952b8e8f59221aec993acc37ccbf31), [3bcc68c](https://github.com/morluto/rea/commit/3bcc68c037e9415c78093cb286069c1fcca6a2f8), [6bae774](https://github.com/morluto/rea/commit/6bae774328724403255590aa2ddad4d7a2aa727f), [e997085](https://github.com/morluto/rea/commit/e9970855ba16ba711816ac30212c5ca17ae085cd), [d1dcebc](https://github.com/morluto/rea/commit/d1dcebce2c0820469ed63e33497a14258971b710), [b123b2e](https://github.com/morluto/rea/commit/b123b2e51cc7d6d2157525664be2df88fb1ba3bd), [049132a](https://github.com/morluto/rea/commit/049132ab43fa962d31774f2b258c15a71217148b), [042dcaa](https://github.com/morluto/rea/commit/042dcaa1ee66f1b8668f08b193f38ab01bceaacb), [acf75e6](https://github.com/morluto/rea/commit/acf75e6700ace28bb1db25e54e56673ac15e0ed3), [1f7f808](https://github.com/morluto/rea/commit/1f7f808c4f3bb5eca6b13bcc71d5cceb2ace6614), [26bffb9](https://github.com/morluto/rea/commit/26bffb94bf80355c35e22a98a8fc629c6210307e)). Thanks @coygeek, @DeryFerd, @jiangjiang248, @kaoru0822-kitauji, @khanukov, @N0zoM1z0, @NIU-123370, @rohitg00, @sx4im.
- **Ghidra setup and analysis:** preserve launcher/JVM diagnostics, Java-property and extension identity, stale-client recovery, UTF-8 bridge output and pending-startup progress. File-offset decompilation is Ghidra-specific; recorded coverage is Linux x64 with Ghidra 12.1.4. Startup/progress/compiler failure paths that use simulations remain unverified with a real engine. ([#1698](https://github.com/morluto/rea/pull/1698), [#1877](https://github.com/morluto/rea/pull/1877), [#1931](https://github.com/morluto/rea/pull/1931), [#2011](https://github.com/morluto/rea/pull/2011), [issue morluto/rea#1992](https://github.com/morluto/rea/issues/1992), [#2014](https://github.com/morluto/rea/pull/2014), [#2101](https://github.com/morluto/rea/pull/2101), [issue morluto/rea#2078](https://github.com/morluto/rea/issues/2078), [#2206](https://github.com/morluto/rea/pull/2206), [issue morluto/rea#2204](https://github.com/morluto/rea/issues/2204), [2187e16](https://github.com/morluto/rea/commit/2187e16f149a7a7b512a4cb45d5f468cc04091fe), [b953f96](https://github.com/morluto/rea/commit/b953f96c0aae27e1876c317a8f7db1d6f292b8ff)). Thanks @EDMND-SRC, @Ha1baraA11, @kaoru0822-kitauji, @lovecontroing, @lyly-bonbon, @N0zoM1z0, @nosmile99, @Petru101, @sx4im, @Zhong-fan.
- **Process and cleanup ownership:** retain private runtime/file owners and detailed incomplete-cleanup observations, bound trace cycles/order state, preserve caller paths, and use monotonic time for budgets while retaining wall dates. Windows affinity/admission ([#2202](https://github.com/morluto/rea/pull/2202), [issue morluto/rea#2181](https://github.com/morluto/rea/issues/2181)) and Bun memory-enforcement paths retain their recorded native-versus-controlled-fixture limits; these fixes are not a general process-ownership guarantee. ([#1700](https://github.com/morluto/rea/pull/1700), [issue morluto/rea#1697](https://github.com/morluto/rea/issues/1697), [#1730](https://github.com/morluto/rea/pull/1730), [#1756](https://github.com/morluto/rea/pull/1756), [issue morluto/rea#1753](https://github.com/morluto/rea/issues/1753), [#1760](https://github.com/morluto/rea/pull/1760), [issue morluto/rea#1757](https://github.com/morluto/rea/issues/1757), [#1826](https://github.com/morluto/rea/pull/1826), [#1907](https://github.com/morluto/rea/pull/1907), [issue morluto/rea#1856](https://github.com/morluto/rea/issues/1856), [#1999](https://github.com/morluto/rea/pull/1999), [issue morluto/rea#1993](https://github.com/morluto/rea/issues/1993), [#2232](https://github.com/morluto/rea/pull/2232), [issue morluto/rea#2170](https://github.com/morluto/rea/issues/2170), [#2252](https://github.com/morluto/rea/pull/2252), [issue morluto/rea#2218](https://github.com/morluto/rea/issues/2218), [#2467](https://github.com/morluto/rea/pull/2467), [issue morluto/rea#2257](https://github.com/morluto/rea/issues/2257), [#2468](https://github.com/morluto/rea/pull/2468), [#2488](https://github.com/morluto/rea/pull/2488), [09b6828](https://github.com/morluto/rea/commit/09b68285166bef2b7633402c8adf7eb25823b47e), [7285e15](https://github.com/morluto/rea/commit/7285e15839a4fecf9940683699b781a60ccc4775), [b5ededd](https://github.com/morluto/rea/commit/b5ededd0f9c28d0f696a205103066928117368ee), [d8c76b6](https://github.com/morluto/rea/commit/d8c76b629639bda4405afa9ae2f7676b694f2f10), [98d8470](https://github.com/morluto/rea/commit/98d8470d36cfbf7cd25820a4ed430aebae20c0c5), [c22111e](https://github.com/morluto/rea/commit/c22111eca2c4bc84a084f12711f405b3e7422a40), [cecc6ec](https://github.com/morluto/rea/commit/cecc6ecbffe84689a173d1634f342e046b0c8ceb), [10935b7](https://github.com/morluto/rea/commit/10935b7b1136accab2a43b308ab380749da0f7ae), [63e796d](https://github.com/morluto/rea/commit/63e796d50ea323ae8dcfef80f86c8154bb106790), [d9e9fb2](https://github.com/morluto/rea/commit/d9e9fb2273543caab4eb21d896c33d462ada6971), [9049dba](https://github.com/morluto/rea/commit/9049dbaf0c3df32357419f9899965df2b611f493), [ed881c6](https://github.com/morluto/rea/commit/ed881c6d2ef6a8adba0c2a241ebc9ab4603b7868)). Thanks @Ashen00001, @charan-rathore, @coygeek, @EDMND-SRC, @JasonBuildAI, @jimpablo, @kaoru0822-kitauji, @N0zoM1z0, @NIU-123370, @Rinkia, @rudycelekli, @sx4im, @xeropresence, @XiaoBinGan, @xthunder0, @hzh666kevin-hue.
- **Native image identity:** preserve universal Mach-O containers and selected slice/subtype identity, chained fixup locations and library ordinals, exact signing text, PE resource capacity and ELF/header framing. Mach-O summary selection and independent slice commands avoid unnecessary delivery/work. Controlled routing/parser fixtures do not establish every installed-provider path. ([#1703](https://github.com/morluto/rea/pull/1703), [issue morluto/rea#1702](https://github.com/morluto/rea/issues/1702), [#1708](https://github.com/morluto/rea/pull/1708), [#1709](https://github.com/morluto/rea/pull/1709), [#1713](https://github.com/morluto/rea/pull/1713), [#1772](https://github.com/morluto/rea/pull/1772), [issue morluto/rea#1765](https://github.com/morluto/rea/issues/1765), [#1784](https://github.com/morluto/rea/pull/1784), [issue morluto/rea#1775](https://github.com/morluto/rea/issues/1775), [#1982](https://github.com/morluto/rea/pull/1982), [#1995](https://github.com/morluto/rea/pull/1995), [issue morluto/rea#1990](https://github.com/morluto/rea/issues/1990), [#2040](https://github.com/morluto/rea/pull/2040), [#2107](https://github.com/morluto/rea/pull/2107), [#2161](https://github.com/morluto/rea/pull/2161), [issue morluto/rea#2105](https://github.com/morluto/rea/issues/2105), [#2248](https://github.com/morluto/rea/pull/2248), [issue morluto/rea#2176](https://github.com/morluto/rea/issues/2176), [#2281](https://github.com/morluto/rea/pull/2281), [issue morluto/rea#2274](https://github.com/morluto/rea/issues/2274), [#2292](https://github.com/morluto/rea/pull/2292), [#2466](https://github.com/morluto/rea/pull/2466), [#2506](https://github.com/morluto/rea/pull/2506), [#2733](https://github.com/morluto/rea/pull/2733), [issue morluto/rea#2661](https://github.com/morluto/rea/issues/2661), [f598bab](https://github.com/morluto/rea/commit/f598bab488e38c378d69346fa3c106f9e18529ed), [108b5f9](https://github.com/morluto/rea/commit/108b5f94254281639badd25b1f448cca35dc89a2), [f1924cf](https://github.com/morluto/rea/commit/f1924cf9c1910d5d29d11e7bf634b51af8d738cf), [8bd5ba2](https://github.com/morluto/rea/commit/8bd5ba2d234cbd9a5bb9dd5daeaa191c3a1cad24), [b38429c](https://github.com/morluto/rea/commit/b38429cb6a0de8f4bb5ae01633e24abe2d7cde46), [8d4d631](https://github.com/morluto/rea/commit/8d4d631606c701aac150cf009d3b80ae4a7ae7d5), [ac57c88](https://github.com/morluto/rea/commit/ac57c88efe64c3125850b54d3d833227e6b8ec51), [dc038d1](https://github.com/morluto/rea/commit/dc038d16bba9046afd28970de9c47f6259a43524), [34a4bb6](https://github.com/morluto/rea/commit/34a4bb6508006fa113463ef20ed799a2994e9ee8), [ebc22d9](https://github.com/morluto/rea/commit/ebc22d99d62225505c0a84f6e39823ba4ef2f829), [c02fb0e](https://github.com/morluto/rea/commit/c02fb0ea8055f98def2b12ad557aed0faefd3639)). Thanks @1NormalGuy, @Aliciawque, @baron, @coygeek, @dhl84, @hemanthrayuduu, @JasonBuildAI, @kaoru0822-kitauji, @N0zoM1z0, @thangldw, @viniciusdsandrade, @XiaoBinGan, @Zburgers.
- **Archive and artifact provenance:** keep physical ZIP occurrence identity, exact signatures/names and partial inventory coverage; bound decompression and reject malformed keys/integers. Directory/ASAR/DMG reads retain ownership through cleanup and publication cancellation. Duplicate ZIP names remain explicitly ambiguous where the public projection cannot represent them; synthetic archive/cleanup cases do not prove every real Unblob or DMG failure path. ([#1726](https://github.com/morluto/rea/pull/1726), [issue morluto/rea#1725](https://github.com/morluto/rea/issues/1725), [#1854](https://github.com/morluto/rea/pull/1854), [issue morluto/rea#1844](https://github.com/morluto/rea/issues/1844), [#1865](https://github.com/morluto/rea/pull/1865), [issue morluto/rea#1860](https://github.com/morluto/rea/issues/1860), [#2069](https://github.com/morluto/rea/pull/2069), [#2106](https://github.com/morluto/rea/pull/2106), [#2144](https://github.com/morluto/rea/pull/2144), [#2365](https://github.com/morluto/rea/pull/2365), [#2493](https://github.com/morluto/rea/pull/2493), [918713a](https://github.com/morluto/rea/commit/918713af376e307ecf24d75e6a0dd9d1b544545b), [eb97da3](https://github.com/morluto/rea/commit/eb97da33881e3d1472d97a7d788ee35944fb6b07), [2260887](https://github.com/morluto/rea/commit/2260887f2092be6a3d9c79509e4ae4a3e52fce7f), [74d7da6](https://github.com/morluto/rea/commit/74d7da6ced0f869936ee0316b00616931b088066), [ed485a3](https://github.com/morluto/rea/commit/ed485a3dc8cc3a4baf8cabe01acb7aa0490b73b3), [921bb95](https://github.com/morluto/rea/commit/921bb9575505065f6cfd57b14d97d80d0f238546), [d8ceb3f](https://github.com/morluto/rea/commit/d8ceb3f276a65c181b146f68c0729ff4b6f9fa61), [9435c7a](https://github.com/morluto/rea/commit/9435c7aae279ba184937c0dfeb713fd574ba3f29), [bb54082](https://github.com/morluto/rea/commit/bb540826d65316c164d2de8ba4ef10f81a8ad073), [a73a55f](https://github.com/morluto/rea/commit/a73a55f53cc2b2e5b7618effe6ecb5cee6e0cee2), [3c57e1a](https://github.com/morluto/rea/commit/3c57e1a8b21b379fca55032d177078e1d8426e64), [ec0b081](https://github.com/morluto/rea/commit/ec0b081371f118e6e5e042fbef3479359c672ddb), [e7882b3](https://github.com/morluto/rea/commit/e7882b34f4ac4a26f903c5f49264acc2c19bb562), [b955b82](https://github.com/morluto/rea/commit/b955b825fc002a4bb408f2910b78768c566c2cd1)). Thanks @1052326311, @1NormalGuy, @baron, @Bortlesboat, @Chaoqi31, @DeryFerd, @kaoru0822-kitauji, @N0zoM1z0, @XiaoBinGan.
- **Setup/uninstall recovery:** preserve unrelated YAML text, caller-selected roots, symlinked data and existing recovery snapshots; explain malformed configuration shapes and retain failed rollback/cleanup owners. Show terminal dry-run plans and each selected skill location. Bunx registration and fault-injected recovery cases remain simulated rather than live-client verification. ([#1731](https://github.com/morluto/rea/pull/1731), [#1754](https://github.com/morluto/rea/pull/1754), [#1821](https://github.com/morluto/rea/pull/1821), [issue morluto/rea#1816](https://github.com/morluto/rea/issues/1816), [#1983](https://github.com/morluto/rea/pull/1983), [#2118](https://github.com/morluto/rea/pull/2118), [issue morluto/rea#2111](https://github.com/morluto/rea/issues/2111), [#2154](https://github.com/morluto/rea/pull/2154), [issue morluto/rea#2152](https://github.com/morluto/rea/issues/2152), [#2157](https://github.com/morluto/rea/pull/2157), [#2182](https://github.com/morluto/rea/pull/2182), [issue morluto/rea#2159](https://github.com/morluto/rea/issues/2159), [#2481](https://github.com/morluto/rea/pull/2481), [bb50813](https://github.com/morluto/rea/commit/bb508134efbba392a404b651d6ec05be8b7eac1e), [c240768](https://github.com/morluto/rea/commit/c2407689cb04df082e8db0b6afa8f8d022f6bcd1), [dcfc5d4](https://github.com/morluto/rea/commit/dcfc5d403ac997c2b08bee4753e57a1a4a8a8538), [21eff3b](https://github.com/morluto/rea/commit/21eff3bae10f58711a35ad44f86e9f96f9bb4790), [94153f1](https://github.com/morluto/rea/commit/94153f1c757e07a6d092e00903aa8b125d4a0afa), [2397137](https://github.com/morluto/rea/commit/23971376158e7815edcbe6c04a8df7c9504650dc), [c339f9e](https://github.com/morluto/rea/commit/c339f9ebe7bd1f30608d6ccb7175da523dd3c485), [f6775a3](https://github.com/morluto/rea/commit/f6775a36694a6a9c620f4248638e8056a6cbcb66)). Thanks @BeqaKs, @Jax367, @kaoru0822-kitauji, @NIU-123370, @shoyann, @viniciusdsandrade, @xeropresence, @zaydiscold.
- **Bun updates:** locate global installations before changing directories and reuse parsed client activation during maintenance. The update flow is covered with package-manager/filesystem simulations; a real Bun global installation update remains unverified. ([#1735](https://github.com/morluto/rea/pull/1735), [e90acae](https://github.com/morluto/rea/commit/e90acaea674f5f7f9f17ea472104afdc1422ada6), [b045df1](https://github.com/morluto/rea/commit/b045df100e1489e08a36d6ff04cea7feecf1332c)). Thanks @xeropresence.
- **JavaScript relationships:** preserve loader-specific package/TypeScript/CommonJS boundaries, exact file-URL and source-map identity, Electron intrinsic receivers/IPC guards and chunk metadata. Index repeated lookups and retain completed facts through resource limits and cancellation. Real Electron integration, Bun worker integration and failure-path cleanup remain unverified where the recorded lanes use fixtures. ([#1740](https://github.com/morluto/rea/pull/1740), [#1759](https://github.com/morluto/rea/pull/1759), [issue morluto/rea#1748](https://github.com/morluto/rea/issues/1748), [#1762](https://github.com/morluto/rea/pull/1762), [issue morluto/rea#1761](https://github.com/morluto/rea/issues/1761), [#1767](https://github.com/morluto/rea/pull/1767), [#1788](https://github.com/morluto/rea/pull/1788), [issue morluto/rea#1781](https://github.com/morluto/rea/issues/1781), [issue morluto/rea#1782](https://github.com/morluto/rea/issues/1782), [#1789](https://github.com/morluto/rea/pull/1789), [#1799](https://github.com/morluto/rea/pull/1799), [issue morluto/rea#1793](https://github.com/morluto/rea/issues/1793), [#1827](https://github.com/morluto/rea/pull/1827), [issue morluto/rea#1802](https://github.com/morluto/rea/issues/1802), [#1833](https://github.com/morluto/rea/pull/1833), [issue morluto/rea#1831](https://github.com/morluto/rea/issues/1831), [#1857](https://github.com/morluto/rea/pull/1857), [issue morluto/rea#1842](https://github.com/morluto/rea/issues/1842), [#1871](https://github.com/morluto/rea/pull/1871), [#1874](https://github.com/morluto/rea/pull/1874), [#1905](https://github.com/morluto/rea/pull/1905), [#1912](https://github.com/morluto/rea/pull/1912), [issue morluto/rea#1909](https://github.com/morluto/rea/issues/1909), [#1915](https://github.com/morluto/rea/pull/1915), [issue morluto/rea#1911](https://github.com/morluto/rea/issues/1911), [#1916](https://github.com/morluto/rea/pull/1916), [#1921](https://github.com/morluto/rea/pull/1921), [#1934](https://github.com/morluto/rea/pull/1934), [#1939](https://github.com/morluto/rea/pull/1939), [issue morluto/rea#1881](https://github.com/morluto/rea/issues/1881), [issue morluto/rea#1882](https://github.com/morluto/rea/issues/1882), [issue morluto/rea#1883](https://github.com/morluto/rea/issues/1883), [#1941](https://github.com/morluto/rea/pull/1941), [#2062](https://github.com/morluto/rea/pull/2062), [issue morluto/rea#2058](https://github.com/morluto/rea/issues/2058), [#2104](https://github.com/morluto/rea/pull/2104), [#2422](https://github.com/morluto/rea/pull/2422), [#2475](https://github.com/morluto/rea/pull/2475), [#2737](https://github.com/morluto/rea/pull/2737), [issue morluto/rea#2663](https://github.com/morluto/rea/issues/2663), [d3088c3](https://github.com/morluto/rea/commit/d3088c3795f6c37cb3b6aec0bb0c371dd0640ee8), [1dfb933](https://github.com/morluto/rea/commit/1dfb933e47db0156cf1cb61f28715b81e740c57b), [f8513a4](https://github.com/morluto/rea/commit/f8513a42dc44451a621127f06edde5d38bc93f69), [b8fab74](https://github.com/morluto/rea/commit/b8fab7429771bb17a7ca0297661de3c9935212af), [42b0ed1](https://github.com/morluto/rea/commit/42b0ed126df7d5b908ac369715378d5e519051b7), [c0904b4](https://github.com/morluto/rea/commit/c0904b4c1aabfccf2f0ab12f44960a0a1fbbbfae), [9f67e44](https://github.com/morluto/rea/commit/9f67e44e5d451d705bae9a579ad9802d5c9f097c), [e4532b5](https://github.com/morluto/rea/commit/e4532b537663026c3e5b784380eecd009c959a36), [1c37914](https://github.com/morluto/rea/commit/1c37914306b3ff257d7a0360f11c9bf2a3f2cac7), [5ab0cbb](https://github.com/morluto/rea/commit/5ab0cbb48ea5a4f11542eac44479b9af48f1dabd), [b7169c8](https://github.com/morluto/rea/commit/b7169c8b747cbc032913b31f008a3b81c77b6219), [f43fc02](https://github.com/morluto/rea/commit/f43fc0247527aebb8efc7d9db04276fe2d489323), [3ae826c](https://github.com/morluto/rea/commit/3ae826ceba6f25b08bce50bcb51001209bc16b57), [1b95302](https://github.com/morluto/rea/commit/1b953021eadf4cac09e20be388d79536903633b3), [eaa9616](https://github.com/morluto/rea/commit/eaa961658578522cecfbba7c7be1acf1c64da65a), [c923fb0](https://github.com/morluto/rea/commit/c923fb0dc9462e302b84a15770131ff899f89455), [c7a890d](https://github.com/morluto/rea/commit/c7a890d12987d9b2d496119c1fbf1a88c0fc298e)). Thanks @Aurantium-47, @baron, @chqcjq, @coygeek, @cyanseek, @fengdwx, @flitzrrr, @immanuwell, @kaoru0822-kitauji, @N0zoM1z0, @Nelglor, @ranmori, @rudycelekli, @shoyann, @xu91102.
- **Browser observations:** preserve effective source-map URLs, empty query/fragment identity, response-body provenance, service-worker policies and explicit uncertainty for malformed events. Bound discovery lifetimes and retain cleanup ownership. Interrupt/retry/failure cases validated with controlled CDP/session fixtures do not establish those paths in real Chromium. ([#1742](https://github.com/morluto/rea/pull/1742), [issue morluto/rea#1729](https://github.com/morluto/rea/issues/1729), [#1829](https://github.com/morluto/rea/pull/1829), [#2074](https://github.com/morluto/rea/pull/2074), [#2099](https://github.com/morluto/rea/pull/2099), [#2239](https://github.com/morluto/rea/pull/2239), [issue morluto/rea#2169](https://github.com/morluto/rea/issues/2169), [#2477](https://github.com/morluto/rea/pull/2477), [486b567](https://github.com/morluto/rea/commit/486b56724c4eaba50719177bcb9eae84d6907297), [a82e37c](https://github.com/morluto/rea/commit/a82e37c8065cc200a0ca3e14e7c4f85e4e468580), [a8d4984](https://github.com/morluto/rea/commit/a8d4984404b611e60b88bc42289c0ba07eee4578), [966f7cc](https://github.com/morluto/rea/commit/966f7cc84b4a8bb0043641cb64d7a7aaf8715e05), [221c856](https://github.com/morluto/rea/commit/221c85648108e9846d9920b0964dafa7f108d581), [951b095](https://github.com/morluto/rea/commit/951b095e11adf9faa9c4a27e212260060aa6b095), [f09bb8a](https://github.com/morluto/rea/commit/f09bb8a99773f79c7306b8799c86f856fe2c0d4b), [6bd2b28](https://github.com/morluto/rea/commit/6bd2b28161411f70ed5dc72e42c2736a1aab758b)). Thanks @1052326311, @chAng-L19, @chqcjq, @coygeek, @evenafterallisaid, @fisforfaheem, @itsF4LCON, @N0zoM1z0, @shayanxcyber.
- **Managed metadata/CIL:** preserve ExportedType and pointer-table ownership, generic/array/function-pointer/modifier signatures, ReadyToRun host identity, metadata ExtraData and signed zero. Charge retained facts and decoded instruction counts instead of broad decoding bounds; malformed layouts remain caller input errors. ([#1743](https://github.com/morluto/rea/pull/1743), [issue morluto/rea#1736](https://github.com/morluto/rea/issues/1736), [#1768](https://github.com/morluto/rea/pull/1768), [#1822](https://github.com/morluto/rea/pull/1822), [issue morluto/rea#1801](https://github.com/morluto/rea/issues/1801), [#1828](https://github.com/morluto/rea/pull/1828), [issue morluto/rea#1812](https://github.com/morluto/rea/issues/1812), [#1835](https://github.com/morluto/rea/pull/1835), [issue morluto/rea#1832](https://github.com/morluto/rea/issues/1832), [#1886](https://github.com/morluto/rea/pull/1886), [#1893](https://github.com/morluto/rea/pull/1893), [#1897](https://github.com/morluto/rea/pull/1897), [issue morluto/rea#1878](https://github.com/morluto/rea/issues/1878), [#1943](https://github.com/morluto/rea/pull/1943), [issue morluto/rea#1940](https://github.com/morluto/rea/issues/1940), [#1985](https://github.com/morluto/rea/pull/1985), [#2194](https://github.com/morluto/rea/pull/2194), [issue morluto/rea#2191](https://github.com/morluto/rea/issues/2191), [#2437](https://github.com/morluto/rea/pull/2437), [52e76c5](https://github.com/morluto/rea/commit/52e76c5d39b58d1685bb5e22bd40d19524dfa2cb)). Thanks @ac1982, @chqcjq, @fisHarly0, @flitzrrr, @JasonBuildAI, @JayOfTheKeyboard, @jhuang-tw, @kaoru0822-kitauji, @N0zoM1z0, @Nelglor.
- **Hopper failure ownership:** preserve startup/process outcomes, retain cleanup leases across one-shot failures, wait for late bridges, and stop owned Linux demo children when their REA owner exits. Denied/crashed/late-bridge/queued-request failure paths use simulated process or bridge fixtures; they are not fresh installed-Hopper verification. ([#1763](https://github.com/morluto/rea/pull/1763), [issue morluto/rea#1751](https://github.com/morluto/rea/issues/1751), [#1817](https://github.com/morluto/rea/pull/1817), [#1866](https://github.com/morluto/rea/pull/1866), [#1927](https://github.com/morluto/rea/pull/1927), [#2474](https://github.com/morluto/rea/pull/2474), [#2499](https://github.com/morluto/rea/pull/2499), [issue morluto/rea#2459](https://github.com/morluto/rea/issues/2459)). Thanks @coygeek, @Joshwong1908, @kaoru0822-kitauji, @N0zoM1z0, @rudycelekli.
- **IDA session boundaries:** retain non-x86 call coverage and unresolved-procedure uncertainty, and bound private universal-Mach-O inputs to the exact selected slice. Transient headless busy recovery was exercised on macOS arm64 IDA Pro 9.1; Linux headless, attached GUI navigation and worker-reaped cleanup paths retain their documented verification gaps. ([#1764](https://github.com/morluto/rea/pull/1764), [issue morluto/rea#1712](https://github.com/morluto/rea/issues/1712), [#1778](https://github.com/morluto/rea/pull/1778), [issue morluto/rea#1766](https://github.com/morluto/rea/issues/1766), [#1785](https://github.com/morluto/rea/pull/1785), [issue morluto/rea#1769](https://github.com/morluto/rea/issues/1769), [#1906](https://github.com/morluto/rea/pull/1906), [issue morluto/rea#1794](https://github.com/morluto/rea/issues/1794), [#2201](https://github.com/morluto/rea/pull/2201), [issue morluto/rea#2139](https://github.com/morluto/rea/issues/2139), [#2291](https://github.com/morluto/rea/pull/2291), [issue morluto/rea#2285](https://github.com/morluto/rea/issues/2285), [#2324](https://github.com/morluto/rea/pull/2324), [12fa8fd](https://github.com/morluto/rea/commit/12fa8fd9f986dbd4ff58165c3ec71a46dc8a8a3b)). Thanks @akushonkamen, @coygeek, @kaoru0822-kitauji, @N0zoM1z0, @nikolay-dementiev, @zecaro.
- **Input and capability diagnostics:** identify invalid paths, format refusals, missing host tools and non-retryable errors without losing caller selections or retryable capability facts. Foreign execution/device mutation annotations remain destructive, analysis-target text is untrusted, and output ordering follows code points rather than host locale. ([#1770](https://github.com/morluto/rea/pull/1770), [#1867](https://github.com/morluto/rea/pull/1867), [#1869](https://github.com/morluto/rea/pull/1869), [issue morluto/rea#1855](https://github.com/morluto/rea/issues/1855), [#1872](https://github.com/morluto/rea/pull/1872), [#1879](https://github.com/morluto/rea/pull/1879), [issue morluto/rea#1853](https://github.com/morluto/rea/issues/1853), [#1899](https://github.com/morluto/rea/pull/1899), [#1902](https://github.com/morluto/rea/pull/1902), [#1904](https://github.com/morluto/rea/pull/1904), [#1908](https://github.com/morluto/rea/pull/1908), [#1925](https://github.com/morluto/rea/pull/1925), [#1987](https://github.com/morluto/rea/pull/1987), [issue morluto/rea#1979](https://github.com/morluto/rea/issues/1979), [#2004](https://github.com/morluto/rea/pull/2004), [issue morluto/rea#2002](https://github.com/morluto/rea/issues/2002), [#2026](https://github.com/morluto/rea/pull/2026), [#2091](https://github.com/morluto/rea/pull/2091), [#2133](https://github.com/morluto/rea/pull/2133), [issue morluto/rea#2129](https://github.com/morluto/rea/issues/2129), [#2145](https://github.com/morluto/rea/pull/2145), [issue morluto/rea#2143](https://github.com/morluto/rea/issues/2143), [#2472](https://github.com/morluto/rea/pull/2472), [#2478](https://github.com/morluto/rea/pull/2478), [issue morluto/rea#2454](https://github.com/morluto/rea/issues/2454), [#2494](https://github.com/morluto/rea/pull/2494), [#2507](https://github.com/morluto/rea/pull/2507), [#2509](https://github.com/morluto/rea/pull/2509), [#2708](https://github.com/morluto/rea/pull/2708), [3d83135](https://github.com/morluto/rea/commit/3d8313598785359c424d7305f503cb69fc8639a6), [c9fed50](https://github.com/morluto/rea/commit/c9fed50691ae014890e91a72534773d2fb58ce44), [8b18171](https://github.com/morluto/rea/commit/8b181716aca2cb467be499cba226abfb5ae19141), [7a2cd1e](https://github.com/morluto/rea/commit/7a2cd1ea0a63e8c44166860b7ee9c7a1aa43492b)). Thanks @astralyx0129, @CassieFox23, @DeryFerd, @dollarsmoney, @fisforfaheem, @flitzrrr, @Ha1baraA11, @jimpablo, @kaoru0822-kitauji, @larinyo, @N0zoM1z0, @rudycelekli, @SII-YDD, @viniciusdsandrade, @XiaoBinGan.
- **Android analysis:** retain decoded values, duplicate-class inventories and executable static initializers, reject malformed JADX UTF-8, and keep inline search provenance. Apktool accepts `--no-strings` and explains host requirements. Windows batch-launcher selection also resolves PATH/PATHEXT before choosing its launch mode. Recorded Apktool coverage is Linux/OpenJDK, and changed JADX/batch failure paths do not establish native Windows behavior. ([#1777](https://github.com/morluto/rea/pull/1777), [issue morluto/rea#1774](https://github.com/morluto/rea/issues/1774), [#1876](https://github.com/morluto/rea/pull/1876), [issue morluto/rea#1846](https://github.com/morluto/rea/issues/1846), [#1890](https://github.com/morluto/rea/pull/1890), [issue morluto/rea#1850](https://github.com/morluto/rea/issues/1850), [#1946](https://github.com/morluto/rea/pull/1946), [issue morluto/rea#1945](https://github.com/morluto/rea/issues/1945), [#2707](https://github.com/morluto/rea/pull/2707), [issue morluto/rea#2672](https://github.com/morluto/rea/issues/2672), [0677468](https://github.com/morluto/rea/commit/0677468877465c8dfbe25c72f142a7b5bc8a3212), [fe8d0bd](https://github.com/morluto/rea/commit/fe8d0bdd36fe12bc0836468d1a702a34fbdc219b), [#2802](https://github.com/morluto/rea/pull/2802)). Thanks @akushonkamen, @coygeek, @HeadmasterEggy, @kaoru0822-kitauji, @N0zoM1z0, @psipher.
- **ADB acquisition:** preserve device paths and directory identity, prevent host destination collisions/overwrites, count UTF-8 logcat bytes and restricted permissions, and evaluate the complete Activity Manager start report. Launch attempts, status, activity and raw failure output remain observations; real-device timeout/mixed-report and Android 8 directory-read paths remain unverified beyond the documented fixtures. ([#1962](https://github.com/morluto/rea/pull/1962), [#2212](https://github.com/morluto/rea/pull/2212), [issue morluto/rea#2175](https://github.com/morluto/rea/issues/2175), [#2221](https://github.com/morluto/rea/pull/2221), [issue morluto/rea#2220](https://github.com/morluto/rea/issues/2220), [#2359](https://github.com/morluto/rea/pull/2359), [c42232f](https://github.com/morluto/rea/commit/c42232f566d55fe7191763649755148c768eef9f), [55b6148](https://github.com/morluto/rea/commit/55b61485e9f7568b69597eef8696ff6abfe53cfd), [3bdc873](https://github.com/morluto/rea/commit/3bdc8730bd98e073522bcb8ba86527be402c0cbe), [c89972c](https://github.com/morluto/rea/commit/c89972c8777d0114566fe76a060849a18bdd2659), [e1cd292](https://github.com/morluto/rea/commit/e1cd292aa6e8f5f13441d408c66126782c8dbb0d)). Thanks @coygeek, @dakjdakd, @DeryFerd, @huiq777, @sx4im.
- **WASM names and diagnostics:** preserve debug/custom-section names including NUL and leading BOM, accept unpadded WABT section rows, and avoid irrelevant tool-remediation advice on unrelated runtime-root failures. ([#1963](https://github.com/morluto/rea/pull/1963), [issue morluto/rea#1956](https://github.com/morluto/rea/issues/1956), [#1967](https://github.com/morluto/rea/pull/1967), [#1981](https://github.com/morluto/rea/pull/1981), [#2084](https://github.com/morluto/rea/pull/2084), [#2102](https://github.com/morluto/rea/pull/2102)). Thanks @1052326311, @bugstan, @N0zoM1z0, @sathariels, @STiFLeR7, @viniciusdsandrade.
- **Source-map and path handling:** retain source-map entries when embedded-content counts differ or an optional `sourceMappingURL` is used, and keep `file:` URLs out of operator-relative path resolution. ([#2873](https://github.com/morluto/rea/pull/2873), [#2886](https://github.com/morluto/rea/pull/2886), [#2882](https://github.com/morluto/rea/pull/2882), [issue morluto/rea#2821](https://github.com/morluto/rea/issues/2821), [issue morluto/rea#2883](https://github.com/morluto/rea/issues/2883), [issue morluto/rea#2577](https://github.com/morluto/rea/issues/2577)). Thanks @JFSAS, @rileygramlich, @shoyann, @shubhamahuja9999, @coygeek.
- **Native metadata:** accept declared facts in native dispatch inventories and share native verification input diagnostics. ([#2906](https://github.com/morluto/rea/pull/2906), [#2901](https://github.com/morluto/rea/pull/2901), [issue morluto/rea#2564](https://github.com/morluto/rea/issues/2564), [issue morluto/rea#2621](https://github.com/morluto/rea/issues/2621)). Thanks @ShitK, @coygeek.
### ⚠ BREAKING CHANGES
- **Runtime attachment state:** `attached` may be `null` in JavaScript, browser and Electron discovery/inspection results. Treat null as unknown; do not coerce it to false. ([e55ef72](https://github.com/morluto/rea/commit/e55ef7213515ba204c3d88a4487b4ee6a2773b06)).
- **Source maps:** original-source and mapping rows preserve a null source name when unavailable. Handle null explicitly instead of expecting an invented unknown-source string; new index fields remain optional when reading older Evidence. ([ee50ed1](https://github.com/morluto/rea/commit/ee50ed121e38cc6e1a2ff0dc8b789f15f71c5402)).
- **Streamed filtered JSON:** `--filter-output` now emits JSON `null` instead of `{}` when the selected value is null; consumers must distinguish a present null from a missing selection. Use `--full-output` when an object envelope is required. ([#2333](https://github.com/morluto/rea/pull/2333)). Thanks @Jackmeson1.
- **Call graphs:** `get_call_graph.normalized_result` is now `{ graph, limitations }`; read traversal-depth groups under `graph` and retain relationship-specific caveats. Contract/fixture validation does not establish new Ghidra or IDA engine coverage. ([5dfb9d0](https://github.com/morluto/rea/commit/5dfb9d050c2cc44a61b183dae765bef4d22a53b7)).
- **MCP discovery:** `binary_session` no longer repeats `server_identity.catalog.tools`. Use the connected server tool list, grouped `tool_availability` and provider capabilities; see [MCP contracts](docs/mcp-contracts.md). Origin: [6395e8d](https://github.com/morluto/rea/commit/6395e8d3007d5ee453477460e7ee1c51cff76be0). ([#1988](https://github.com/morluto/rea/pull/1988)). Thanks @kaoru0822-kitauji, @N0zoM1z0.
- **Provider admission:** MIPS targets with unresolved GNU ABI attributes are refused. Supply supported ABI declarations. Saved Hopper databases on macOS are unavailable because the launcher cannot run the analysis bridge when opening a database; select an executable target instead. Database support requires a script-capable database launch route. ([d4bd65c](https://github.com/morluto/rea/commit/d4bd65c53c0254d0195cc7ab8da163dcadfd3b9b)).
- **Segment extents:** `binary_overview` segment length may be null when Hopper coordinates cannot establish a finite extent in one address space. Preserve unknown rather than converting it to zero or a saturated value. ([36db19c](https://github.com/morluto/rea/commit/36db19c59)).
- **Invocation identity:** `server.command_path` may be null, and registration/setup statuses or doctor classifications may be unknown. Preserve these states and establish the actual invocation before treating a registration as aligned. ([187f113](https://github.com/morluto/rea/commit/187f113f7)). Thanks @hemv-857, @qorvr.
- **ADB installation:** the optional `replace` input is unchanged. Results now require `replace_requested` to record the requested `adb install -r` mode; `replaced` is nullable because prior device package state was not observed. Update result readers to accept null instead of treating reinstall intent as proof of replacement. ([864314a](https://github.com/morluto/rea/commit/864314ac857924a7e6e65bfb37e8dc720fcc21e7)).
- **Managed/native verification:** unresolved P/Invoke results may retain `matched_native` and candidate facts when module identity is unknown. Interpret `status`, `basis` and `limitations`; `module-identity-unknown` does not verify correspondence, and unresolved no longer implies empty observations. ([3f6b575](https://github.com/morluto/rea/commit/3f6b5755b3e699bd9373641acffe0f2c5d2ab094)).
Full contribution record: [893 commits and 529 merged PRs](https://github.com/morluto/rea/blob/rea-agents-6.5.0/docs/releases/6.5.0.contributions.json).
## [6.4.0](https://github.com/morluto/rea/compare/rea-agents-6.3.0...rea-agents-6.4.0) (2026-10-11)
### Highlights
+76
View File
@@ -24,6 +24,82 @@ costs change.
When adding or changing an MCP tool, follow the [tool design guide](docs/tool-design.md) and preserve the canonical contracts and generated catalog.
## Reporting issues
Use the issue form that fits the report and keep it concise. For bugs, include
the actual result, the shortest reproduction you ran, the tested version and
environment, and the smallest useful output, artifact, screenshot, or source
reference. Ground expected behavior in documentation, source, or a known-good
version when available. State observed impact and frequency; keep untested scope
and suspected causes explicit.
Incomplete reports and intermittent failures are welcome. If a narrative answer
or supporting evidence is unavailable, write `NOT_ENOUGH_INFO` and explain the
gap. Clearly label source-only findings and proposed checks; do not claim runtime
reproduction or results you did not observe. A root-cause analysis or fix is not
required to report a problem. Documentation reports should identify the relevant
passage or missing guidance and explain the correction or confusion.
Feature and design proposals can start with a problem and desired outcome.
Examples, prior art, and measurements help when available; ideas without them
are welcome. Maintainers can investigate reports and help resolve open questions.
Human ownership and readiness declarations apply to PRs as described below.
## Pull request ownership and readiness
AI-assisted contributions are welcome. Every PR must have a human owner who
takes responsibility for the complete change, including code, tests,
documentation, and claims produced with AI assistance. The PR author is the
owner unless another human is named in the PR and explicitly accepts ownership.
These requirements also apply to maintainer-authored PRs.
Before requesting maintainer review, the human owner must:
- Review the final diff and relevant existing code. Be able to explain the
problem, the chosen approach, how it fits REA, and meaningful edge cases.
- Provide evidence that the change works. Summarize what was checked, the
observed results, and meaningful gaps in the PR body. Use focused tests,
representative CLI/MCP output, CI artifacts, screenshots, or other checks
appropriate to the change. For fixes, show the original failure and why the
regression coverage catches it when practical. Documentation-only changes
can use source, link, or rendered-document checks. Label source-derived
conclusions and proposed checks accurately; never report an unrun check as
passing. Follow the [verification requirements](#development-feedback-and-pr-verification)
for required checks and real-provider coverage.
- Complete the **Author readiness** declaration in the PR template, explicitly
stating that the PR is ready for initial review.
Keep the PR description concise and current. Explain the problem, observable
outcome, approach, and useful evidence; link long logs or put them in optional
details. Address actionable automated review findings and explain disagreements.
Human ownership and review remain required when CI or an automated reviewer
reports success. Readiness requests maintainer review; it does not grant scope
approval or permission to merge.
Draft PRs may request early feedback with open questions and incomplete checks.
Leave the readiness declaration unchecked until the human owner has completed
it. The declaration records initial readiness; later pushes do not invalidate
it or require another human sign-off. Keep the PR description and affected
validation current as work changes. AI agents must leave human declarations to
the human owner and must not attest to a human review on their behalf.
The `PR readiness` check reports pending for drafts and requires a named human
owner and all three template declarations for non-drafts. It checks that the
declaration is filled, not whether the code or evidence is correct. Existing PRs
can copy the **Author readiness** section from the current template.
Open PRs must have distinct head commits because GitHub shares commit statuses.
Maintainers enable merge enforcement after the workflow is merged: run
`gh workflow run pr-readiness.yml -f pull_request_number=NUMBER` for existing
open PRs, then add `PR readiness` from GitHub Actions to the branch's required
status checks alongside its existing checks. Deploy the workflow to each
protected target branch before requiring it there. The workflow reads PR
metadata using trusted default-branch code and writes only a commit status.
Maintainers may return a PR to the author or close it when the problem is
unsupported, validation is missing, or the owner cannot explain the change.
Contributors are responsible for completing this work before requesting review.
## Contribution scope
REA focuses on software structure, runtime behavior, and evidence through
+40 -6
View File
@@ -567,18 +567,27 @@ def trace(debugger, config):
before_digest = _hash_file(config["executable"])
before_identity = _file_identity(config["executable"])
except OSError as error:
return {"status": "target-integrity-error", "error": str(error)}
return {
"status": "target-integrity-error",
"error": str(error),
"target_process_created": False,
}
if before_digest != config["expected_sha256"]:
return {
"status": "target-integrity-error",
"error": "selected executable digest changed before LLDB target creation",
"target_process_created": False,
}
error = lldb.SBError()
target = debugger.CreateTarget(
config["executable"], config["architecture"], None, False, error
)
if not target.IsValid():
return {"status": "target-error", "error": error.GetCString()}
return {
"status": "target-error",
"error": error.GetCString(),
"target_process_created": False,
}
module_path_before, module_file_identity_before, module_uuid_before = (
_module_identity(target)
)
@@ -589,6 +598,7 @@ def trace(debugger, config):
return {
"status": "target-integrity-error",
"error": "LLDB executable module resolved to a different pre-launch file identity",
"target_process_created": False,
}
created = [_create_breakpoints(target, spec) for spec in config["breakpoints"]]
breakpoint_index = {}
@@ -622,12 +632,24 @@ def trace(debugger, config):
if not error.Success() or not process.IsValid():
stdout.close()
stderr.close()
return {"status": "launch-error", "error": error.GetCString()}
return {
"status": "launch-error",
"error": error.GetCString(),
**(
{"target_process_created": True}
if process.IsValid()
else {}
),
}
launch_failure = _failed_launch_description(process)
if launch_failure is not None:
stdout.close(expected_exit=True)
stderr.close(expected_exit=True)
return {"status": "launch-error", "error": launch_failure}
return {
"status": "launch-error",
"error": launch_failure,
"target_process_created": True,
}
stdout.mark_launched()
stderr.mark_launched()
# Publish the PID before any post-launch file or module inspection. The
@@ -644,6 +666,7 @@ def trace(debugger, config):
return {
"status": "target-identity-unavailable",
"error": "host could not establish the launched process identity; target was stopped",
"target_process_created": True,
}
try:
after_digest = _hash_file(config["executable"])
@@ -652,7 +675,11 @@ def trace(debugger, config):
process.Kill()
stdout.close(expected_exit=process.GetState() == lldb.eStateExited)
stderr.close(expected_exit=process.GetState() == lldb.eStateExited)
return {"status": "target-integrity-error", "error": str(identity_error)}
return {
"status": "target-integrity-error",
"error": str(identity_error),
"target_process_created": True,
}
if after_digest != config["expected_sha256"] or after_identity != before_identity:
process.Kill()
_wait_for_exit(listener, process, 5)
@@ -661,6 +688,7 @@ def trace(debugger, config):
return {
"status": "target-integrity-error",
"error": "selected executable digest or file identity changed during LLDB launch",
"target_process_created": True,
}
module_path_after, module_file_identity_after, module_uuid_after = (
_module_identity(target)
@@ -676,6 +704,7 @@ def trace(debugger, config):
return {
"status": "target-integrity-error",
"error": "LLDB executable module resolved to a different file identity",
"target_process_created": True,
}
if (
module_uuid_before is not None
@@ -689,6 +718,7 @@ def trace(debugger, config):
return {
"status": "target-integrity-error",
"error": "LLDB executable module identity changed during launch",
"target_process_created": True,
}
context = {
"target": target,
@@ -748,7 +778,11 @@ def trace(debugger, config):
# exit code; preserve the debugger's concrete launch denial.
launch_failure = _failed_launch_description(process)
if launch_failure is not None:
return {"status": "launch-error", "error": launch_failure}
return {
"status": "launch-error",
"error": launch_failure,
"target_process_created": True,
}
launch_identity_stable = (
module_path_before is not None
and module_path_after == module_path_before
+37
View File
@@ -8,6 +8,7 @@ import sys
import tempfile
import types
import unittest
from unittest.mock import patch
fake_lldb = sys.modules.setdefault("lldb", types.ModuleType("lldb"))
@@ -22,6 +23,42 @@ spec.loader.exec_module(tracer)
class NativeTraceBudgetTests(unittest.TestCase):
def test_pre_target_integrity_failure_reports_no_process_created(self):
original_debugger = getattr(tracer.lldb, "SBDebugger", None)
class DebuggerApi:
@staticmethod
def SetInternalVariable(*_arguments):
return None
class Debugger:
def SetAsync(self, _enabled):
return None
def GetInstanceName(self):
return "fixture"
def CreateTarget(self, *_arguments):
raise AssertionError("integrity failure precedes target creation")
tracer.lldb.SBDebugger = DebuggerApi
try:
with patch.object(tracer, "_hash_file", return_value="changed"), patch.object(
tracer, "_file_identity", return_value=(1, 2)
):
result = tracer.trace(
Debugger(),
{"executable": "fixture", "expected_sha256": "expected"},
)
finally:
if original_debugger is None:
del tracer.lldb.SBDebugger
else:
tracer.lldb.SBDebugger = original_debugger
self.assertEqual(result["status"], "target-integrity-error")
self.assertIs(result["target_process_created"], False)
def test_journal_checks_exact_byte_budget_before_writing(self):
record = {"kind": "other-stop", "reason": "signal"}
encoded_size = len((tracer.json.dumps(record) + "\n").encode("utf-8"))
+9 -1
View File
@@ -26,6 +26,9 @@ Readiness is `rea inspect-adb-client`: it runs `adb version` and reports the
resolved binary, its version, and where it was selected from. A missing
binary reports `capability_unavailable` with the `REA_ADB_PATH` remediation
rather than attempting an installation.
The `binary_session` inventory uses the same bounded client-version probe; it
does not query devices, start the adb server, or establish that a requested
serial is reachable. Device state is checked by each device operation.
## Answer an analyst question
@@ -94,12 +97,17 @@ existing analysis families directly, for example
| `capture_adb_screen` | read | One `screencap -p` frame (exec-out, no device-side file) with dimensions |
| `collect_adb_bugreport` | read | Device-generated bugreport zip, digested |
| `push_adb_file` | **mutates device** | One local file to a device path (e.g. pushing tools into `/data/local/tmp`); checks for existing targets and refuses them without explicit overwrite (the check is not atomic); compares digests when device `sha256sum` is available |
| `install_adb_package` | **mutates device** | Local APK via `adb install [-r]`; preserves the device's Success/Failure verdict |
| `install_adb_package` | **mutates device** | Local APK via `adb install [-r]`; preserves its Success/Failure verdict, records `replace_requested`, and leaves actual replacement `null` because prior package state is not observed |
| `uninstall_adb_package` | **mutates device** | Removes a package and its data for user 0 |
| `start_adb_app` | **mutates device** | Resolves the package's launcher activity (`cmd package resolve-activity --brief`) and starts it (`am start -n`) |
| `start_adb_activity` | **mutates device** | Starts an intent (`am start -a` with optional URI, component, and up to 16 typed extras); preserves the activity manager's verdict |
| `stop_adb_app` | **mutates device** | Force-stops every process of a package, ending a dynamic-analysis run |
For `install_adb_package`, `replace_requested` records whether `-r` was sent.
The `replaced` result is `null`: the successful install response does not say
whether the package was installed fresh or replaced, and REA does not query the
device first. The install verdict and host APK digest remain available.
## Interpret the result
- **Directory names retain whitespace and newlines.** Symlink targets do too,
+2
View File
@@ -21,6 +21,8 @@ Readiness is `rea inspect-apktool-client`: it runs `apktool --version` and
reports the resolved launcher, its selection source, and its version. A
missing launcher reports `capability_unavailable` with the
`REA_APKTOOL_COMMAND` remediation rather than attempting an installation.
`binary_session` uses the same host probe for tool availability; it does not
validate a particular APK or guarantee that the selected input can be decoded.
On Windows, a `.bat` or `.cmd` launcher (such as Chocolatey's `apktool.bat`)
runs through an explicit `cmd.exe /d /s /c` call. That call has no AutoRun,
+23
View File
@@ -20,6 +20,17 @@ Inspect native SAZ or HAR 1.2 offline with REA, or connect Everywhere's separate
- `observe_web_session` / `rea observe-web-session` arms an observation window of the requested duration for an external user action and records ordered reload, SPA navigation, redirect, failure, lifecycle, and target-termination metadata.
- `discover_webmcp_tools` / `rea discover-webmcp-tools` uses the experimental CDP WebMCP domain to return every in-scope registration and its complete observed `input_schema` declaration and canonical `input_schema_sha256` digest inline. Declarations are untrusted JSON data, including declared types, required fields, enums, examples and reference URLs. REA does not resolve schema references. These fields replace `input_schema_shape`. Registrations with the same name and URL in distinct observed frames retain separate identities. `tool_key` includes the observed frame ID; it is not a stable identity across navigation or browser sessions. Same-frame registration updates replace that owner only, and removal leaves other owners intact. Declared `readOnly`, `untrustedContent`, `consequential`, and `autosubmit` annotation hints are reported per registration: `true`/`false` are declared values, `null` means the declaration did not provide the hint in that capture, and a missing `consequential` key means the hint was not recorded, either in a historical capture or because the observed annotation was invalid. Consult discovery completeness and exclusions to distinguish these cases. Malformed annotation values retain the registration with incomplete coverage and an `invalid_protocol_value` exclusion. Other malformed protocol payloads can fail discovery explicitly. REA never exposes `WebMCP.invokeTool`.
- `compare_web_captures` / `rea compare-web-captures` compares passive captures or exact-step-aligned browser scenarios. Scenario results commit reproducible literal normalization and expose alignment failures plus artifact-level action, screenshot, DOM, accessibility, URL, history, storage, and event differences. Missing or truncated evidence is never treated as equivalence.
Passive DOM comparison checks complete node inventories and complete frame
parent/child structure independently. Frame IDs and sibling enumeration order
are capture-local and do not establish a change; duplicate IDs, missing parents,
and cyclic ancestry remain unknown. A complete frame difference can still prove
change when DOM nodes are incomplete, and vice versa. Partial DOM or metadata
collection digests do not prove modification: a missing node or link can result
from a protocol omission. Rejected DOM facts also mark derived URL and agent-hint
metadata incomplete, while valid facts from other nodes and documents remain
available in the capture.
- `capture_web_screenshot` / `rea capture-web-screenshot` returns a content-addressed visible-viewport PNG.
- `compare_web_screenshots` / `rea compare-web-screenshots` performs bounded local PNG pixel comparison without OCR or external services.
- The CLI comparison commands accept each capture, screenshot artifact or normalization policy as inline JSON or a JSON file path. Use files for screenshot artifacts and scenario captures, whose embedded PNG bytes can exceed the host's command-line length limit.
@@ -294,6 +305,18 @@ validation and resource bounds. Omitted script metadata makes its affected
section explicitly truncated.
Disallowed-origin entries are filtered before retention.
Resource-tree inventories distinguish a valid empty `resources` array from a
missing or malformed collection. Invalid resource entries are omitted with
incomplete coverage, while valid resources from that frame and other frames
remain available. Comparing an incomplete resource inventory cannot prove
addition, removal, or equality; a complete empty inventory can prove removal.
Resource records require a nonempty type and a string MIME type; absent or
malformed required fields are recorded as omissions instead of invented defaults.
An explicitly empty MIME type is valid. The optional content size may be absent;
an invalid supplied size remains unknown with incomplete resource coverage while
the resource's other facts are retained. Zero and nonnegative fractional sizes
are preserved.
CDP discovery, connection, and commands wait for the peer or caller cancellation;
a stalled peer can leave an operation waiting. WebSocket messages remain subject
to the [64 MiB limit](#request-boundary). REA does not impose a message-count or
+7 -3
View File
@@ -152,9 +152,13 @@ overrides it. In MCP, pass `provider_id` to `open_binary`:
The selected provider remains bound to the session until an explicit switch
or close. Provider failures are returned with their original reason. For an
`ambiguous` selection error, choose from `details.candidate_ids`; for
`provider_unavailable`, run `rea doctor --provider ID --json` to diagnose the
selected engine. See [task readiness](installation.md#check-readiness-for-your-task)
and [provider selection](mcp-contracts.md#identity-and-discovery).
`provider_unavailable`, follow `message`, `remediation.action`, and the original
reasons in `details.rejections`. For Hopper, Ghidra, IDA, and JADX, run
`rea doctor --provider ID --json` to diagnose the selected engine. Other adapters,
such as `pwntools-elf`, report their configuration requirements directly and are
not accepted by `doctor --provider`. See
[task readiness](installation.md#check-readiness-for-your-task) and
[provider selection](mcp-contracts.md#identity-and-discovery).
The session reports work still in progress through `analysis_activity`.
A client timeout can end its wait while the provider continues analyzing.
+35 -13
View File
@@ -122,6 +122,16 @@ See [client configuration](client-configuration.md) for client-specific roots,
profiles, and environment overrides. A mixed selection plans each distinct
destination. Selecting the skill without a client uses `~/.agents/skills`.
Setup leaves existing skill copies in other locations untouched.
Setup refuses symbolic links at the selected skill directory or its managed
`references` directory, matching uninstall and update's ownership rules. Use a
regular skill directory, or select `--skill=false` to configure the client
registration without installing the bundled skill.
The final selected directories are checked before approval and again before
setup changes any client or Hopper installation. Unselected client destinations
do not block other setup choices. Doctor reports linked or non-directory skill
paths as `unmanaged`, names the affected paths, and provides those alternatives;
an unreadable skill identity remains `unknown`. Uninstall retains and reports
files it cannot safely remove, including their ownership manifest.
`rea setup --skill-client claude_code --yes --json` refreshes and verifies the
selected skill destination independently of MCP registration selection. The
@@ -249,14 +259,14 @@ Register with the variable unset:
<!-- x-release-please-start-version -->
```bash
claude mcp add --scope local rea -- npx -y rea-agents@6.4.0 mcp
claude mcp add --scope local rea -- npx -y rea-agents@6.5.0 mcp
```
On native Windows, use the same project directory and wrap the package-runner
shim so the stdio client can launch it:
```powershell
claude mcp add --scope local rea -- cmd.exe /d /c npx -y rea-agents@6.4.0 mcp
claude mcp add --scope local rea -- cmd.exe /d /c npx -y rea-agents@6.5.0 mcp
```
<!-- x-release-please-end -->
@@ -388,8 +398,9 @@ The npm release checked on **2026-10-11** was **6.4.0**, published from
[`a56f7bb6`](https://github.com/morluto/rea/commit/a56f7bb668ab23fec8d33f7af353ac7ed2fe1895).
The actual registry package accepted dry-run setup plans for the 17 client IDs
in that release, including Qwen Code, Pi and Hermes. DeepSeek Harness (`dsh`)
is on main and is not in npm 6.4.0; `rea setup --client dsh` against `@latest`
reports an unsupported client until a release containing it is published.
is included from 6.5.0 and was absent from the checked npm 6.4.0 package.
The 6.4.0 registry check does not establish 6.5.0 publication or live DeepSeek
API integration.
OpenCode's plan selected
`OPENCODE_CONFIG_DIR` even when that directory did not yet exist. These checks
used an isolated home on macOS with Node 24; they verify planning and argument
@@ -567,8 +578,13 @@ sessions before removing that recovery directory and retrying.
### Launcher paths and troubleshooting
On macOS, REA uses
`/Applications/Hopper Disassembler.app/Contents/MacOS/hopper` by default.
On macOS, REA checks executable Hopper launchers in this order: the canonical
system application, the canonical application installed by `rea setup` under
`~/Applications`, then other `Hopper*.app` bundles in `/Applications` and
`~/Applications`. The Homebrew cask location is an optional `rea doctor`
diagnostic; to use a Homebrew-installed launcher for CLI/MCP startup, set
`HOPPER_LAUNCHER_PATH` to the path reported by doctor. REA does not run Homebrew
while loading runtime configuration.
On Linux, it prefers executable `/opt/hopper/bin/Hopper`, then executable
`~/.local/share/rea/hopper/bin/Hopper`, followed by `/opt/hopper-v4/bin/hopper`
and `/usr/bin/hopper`; if none exists, the former remains the diagnostic
@@ -629,12 +645,18 @@ memory has no original file offset. For FAT Mach-O, offsets refer to the origina
container file. Results retain Hopper's image-relative offset, the observed slice
base, and the source executable path.
Loader selection reads the Mach-O container header, including FAT files with a
single architecture. For FAT64, REA validates the architecture table and selected
Mach-O header, prepares a private thin image, and loads it with Hopper's native
Mach-O loader. It checks the entire source's SHA-256 while copying the slice;
source identity and reported offsets still refer to the original container.
An ambiguous architecture subtype requires an explicitly extracted thin image;
REA does not guess. Configured loader arguments remain explicit overrides.
single architecture. For FAT32 and FAT64 containers, REA validates the
architecture table and selected Mach-O header, prepares a private thin image,
and loads it with Hopper's native Mach-O loader instead of Hopper's implicit
FAT slice choice. For arm64 targets, REA ranks subtype bases 0 (plain arm64) and
2 (arm64e), ignoring capability bits for ranking while retaining the raw subtype
for slice identity. These slices' function-entry encodings decode under
`--aarch64`, ahead of Apple's arm64e.x1 variant (base subtype 12), which is
selected only when it is the arm64 family's only candidate. REA checks the
entire source's SHA-256 while copying the slice; source identity and reported
offsets still refer to the original container. Any other ambiguous architecture
subtype requires an explicitly extracted thin image; REA does not guess.
Configured loader arguments remain explicit overrides.
FAT64 is distinct from the CPU architecture: FAT32 containers can contain 64-bit
executable images. This preparation avoids the Raw Binary loader dialog observed
with native FAT64 loading on Hopper 6.1.0-demo.
@@ -1033,7 +1055,7 @@ For a client that requires manual configuration, use:
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@6.4.0", "mcp"]
"args": ["-y", "rea-agents@6.5.0", "mcp"]
}
}
}
+8 -1
View File
@@ -86,7 +86,14 @@ outside this mutation pass.
Function fingerprints commit normalized syntax, control-flow shape, relation
shape, literal sets, arity, and detected effects without using local names or
source offsets. Equal duplicate fingerprints remain ambiguous. Dynamic
source offsets. Function-local directive literals contribute their values while
preserving escape spelling: `"use\x20strict"` differs from `"use strict"`.
Quote style, formatting, and local names remain normalized. Equal duplicate
fingerprints remain ambiguous.
Fingerprints are static candidates, not behavioral-equivalence proofs or a
version-independent identity. Re-analyze affected sources and obtain new seeds
after analyzer corrections; previously retained Evidence remains queryable using
its original component commitments. Dynamic
calls/properties/names, parser recovery, unresolved handles, and unsupported API
shapes stay explicit unknowns; a missing relation is never reported as proven
behavioral absence.
@@ -273,6 +273,12 @@ RPC client syntax also contributes network endpoint candidates, in both
`OperationDefinition` document object. A graphql-tag `loc.source.body` copy is
not reported twice. Anonymous operations are not reported.
Literal link options and precompiled document fields follow object property
order: the final definition owns the key. A later spread, dynamic computed key,
method, or accessor can make a field unknown, so an overwritten literal is not
reported as the endpoint or operation. A final explicit property after a spread
or dynamic key remains usable.
A procedure path is the server-side procedure name, not a resolved URL; the
transport URL depends on the link configuration and batching. When the proxy is
reached through a member expression, such as a webpack module binding
+6 -2
View File
@@ -323,6 +323,9 @@ function implements a specific managed method.
`verify_managed_native_boundaries` compares declared P/Invoke module and entry
point identities with supplied native export or function evidence. Name/module
matches establish declaration links; they do not establish that a call executed.
When either side lacks a usable module identity, a matching name remains
unresolved and retains the candidate evidence; REA reports a module mismatch
only when both identities are available and differ.
Metadata tokens are not native addresses. Native body mapping for C++/CLI,
ReadyToRun, NativeAOT, and IL2CPP requires separately verified format and provider
@@ -360,8 +363,9 @@ these indirections for member ownership and local attribute-constructor types;
field and method tokens still identify the original definition rows.
`field_list` and `method_list` retain the metadata's list coordinates, which
refer to the pointer table when present rather than necessarily contiguous
definition rows. Malformed or ambiguous pointers leave the owner unknown with
partial coverage while preserving readable member and attribute facts.
definition rows. Invalid or nonmonotonic list coordinates and malformed or
ambiguous pointers leave affected owners unknown with partial coverage while
preserving readable member and attribute facts.
The retained inverse indexes are admitted against the existing representation
budget before allocation.
+16
View File
@@ -54,6 +54,22 @@ resource limit; use selected fields, a display filter or an input-packet limit
when dissection output is too large. Each call checks the recording's current
digest, so pages from different source digests must not be combined.
For a question about a known input prefix, select `packet_limit` for that
prefix, the fields needed for the answer, and a matching output window. For
example, a question about the first twelve PostgreSQL TCP packets within
original frames 1–2,000 can use fields `tcp.srcport`, `tcp.dstport`, `tcp.flags`,
filter `tcp.port == 5432`, `packet_limit: 2000` and `match_limit: 12`.
The matching count then describes that input prefix. A filter containing
`frame.number <= 2000` without an input limit still reads the entire capture;
returning twelve rows does not limit producer output. These controls answer a
bounded question and must not be used to infer whole-capture absence.
Discover unfamiliar field names with `inspect_packet_fields`, using an exact
protocol and a narrow query. If the installed names are already established,
direct field selection avoids an additional glossary probe. Count discovery
and any retries when comparing investigation cost; a small page alone does
not establish a faster workflow.
Additional selections:
- `display_filter`: Wireshark display-filter syntax; omitted means no filter.
+14 -3
View File
@@ -383,13 +383,24 @@ exit comparison like `settlement.elapsed_ms`.
Every capture requires `truncation_details`, with separate accounting for:
- `raw_terminal`: original UTF-8 PTY chunk bytes and observed/retained chunk
counts. A chunk that does not fit `limits.output_bytes` is omitted whole;
a later smaller chunk can still fit.
counts. A chunk that does not fit either its remaining original-text budget
or normalized-text budget is omitted whole; a later smaller chunk can still
fit. A normalized-text omission also omits its original chunk, so raw and
normalized coverage can both be incomplete for that event.
- `normalized_terminal`: normalized comparison-text bytes retained from raw
chunks. This is optional when reading historical captures. A raw-fitting
chunk is omitted if an ordered normalization stage would exceed the remaining
`output_bytes` budget before the final normalized size is known. Its final
size is unknown, not assumed to equal the budget. The capture records the
omitted-frame count and a limitation; later smaller chunks remain eligible.
- `rendered_terminal`: cumulative UTF-8 serialized-state and visible-line
bytes, plus observed/retained frame counts. This is independent of the raw
chunk budget and does not count JSON encoding. Repeated screen snapshots can
exhaust it even when all raw output fits. Rendered states use retained PTY
input, so raw omissions also limit rendered coverage.
input, so raw omissions also limit rendered coverage. When bounded
normalization omits a rendered frame before its final size is known,
`unknown_size_frames` reports that count; `observed_bytes` includes only
rendered frame sizes that were determined exactly.
- `filesystem_before` and `filesystem_after`: file-count/depth limits,
enumeration failures, whole-file hash budget and bytes successfully hashed.
Directory names are read incrementally within the remaining entry capacity.
+3 -1
View File
@@ -66,6 +66,8 @@ REA는 에이전트를 분석 도구에 연결해 네이티브 바이너리, Jav
## 빠른 시작
Linux에서 Ghidra, JADX, 펌웨어 도구를 실행하거나 정적 JavaScript/Electron 분석을 하려면, 프로세스 소유권 확인을 위해 REA의 `PATH`에 procps 호환 `ps`가 있어야 합니다. 해결 방법은 `docs/installation.md`의 "Linux owned-process prerequisite" 항목에 있습니다.
### 에이전트 설정
Node.js와 npm이 설치된 환경에서 다음 명령을 실행하세요.
@@ -151,7 +153,7 @@ REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니
| EVM 바이트코드 | 디스패치 선택자, 바이트 오프셋, 추론한 인자와 상태 변경 가능 여부 | Linux x64의 로컬 원시 바이트/16진수 입력 파일; [오프라인 EVM 가이드](../evm-bytecode.md) |
| SQLite 데이터베이스 | 테이블, 열, 인덱스, 뷰, 트리거, 선택한 일반 테이블의 행 | 로컬 데이터베이스/WAL 스냅샷; Node.js 24.15 이상의 24.x 또는 26 이상(네이티브 SQLite 한도 적용) |
| 기록된 Linux 크래시 | 원시 note 레코드, 기록된 모든 스레드의 레지스터/시그널, 선택적 매핑 후보 | Linux x64에서 호출자가 제공하는 pwntools; GDB/pwndbg는 선택 사항; [기록된 크래시](../recorded-crashes.md) |
| JavaScript / Electron | 모듈, 임포트, 소스 맵, 라우트, IPC, 네이티브 애드온 관계 | Node.js와 npm; [애플리케이션 분석](../javascript-application-workflows.md) |
| JavaScript / Electron | 모듈, 임포트, 소스 맵, 라우트, IPC, 네이티브 애드온 관계 | Node.js와 npm; Linux: procps 호환 `ps`; [애플리케이션 분석](../javascript-application-workflows.md) |
| 웹사이트 | 페이지 구조, 스크립트, 네트워크 관찰 결과, 요청한 스크린샷 | Chrome 계열 브라우저; [브라우저 분석](../browser-observation.md) |
| 저장된 네트워크 캡처 | 요청, 응답, 접근 가능한 페이로드, 소스 위치 | HAR 또는 Fiddler Everywhere SAZ; mitmproxy 네이티브 캡처에는 Linux의 mitmdump; [캡처 가이드](../web-network-captures.md) |
| .NET 어셈블리 | 메타데이터, CIL 명령, 선언된 네이티브 의존성, 빌드 비교 | 별도 도구 불필요(정적 검사만); [관리 코드 가이드](../managed-code-analysis.md) |
File diff suppressed because it is too large. Load diff
+3
View File
@@ -10,6 +10,9 @@ SHA returned by Release Please. Before either registry is published, the release
run calls full CI qualification at that exact revision, including coverage,
portable providers, and every native package host. Later main commits cannot
change or cancel that proof.
The required reverse-tools lane runs fresh coverage and both mutation
preflights. Complete mutation sweeps run separately on a schedule or manual
dispatch and do not block publication; see [the testing guide](testing.md).
Ordinary main pushes and closed, unmerged PRs cannot publish packages. Check
both publication jobs and the published-package canary; a merged PR or GitHub
tag alone does not establish that publication finished. Retry failed jobs in
+27
View File
@@ -280,6 +280,33 @@ focused suite also exercises affected MCP routes and the Python Cutter bridge.
Contract, integration, and real-provider lanes complement these focused tests;
simulated providers do not establish real-provider behavior.
Required CI retains fresh reverse-tools coverage and preflights both canonical
Stryker configurations with `--dryRunOnly`. These preflights resolve the owned
targets, instrument the source, and execute the real initial tests. Invalid
configuration, target resolution, and initial test failures still fail the job.
Preflights execute no mutants and produce no mutation-outcome report. Both fast
feedback and full qualification, including exact-revision release qualification,
require coverage and both preflights. A failed, canceled, skipped or missing
selected quality job still fails the required gate.
The separate **Mutation sweep** workflow runs both complete configurations daily
and on manual dispatch, in two isolated jobs. It is a maintenance check outside
the PR and release publication gates. `ci:full` requests full regression,
coverage and provider qualification; it does not request a complete mutation
sweep. Use the Mutation sweep workflow to request that result. Targets,
checkers, concurrency, deadlines and the existing non-blocking mutation-score
threshold are unchanged. `npm run test:mutation:reverse-tools` also retains both
complete passes for local callers.
The two passes write separate JSON and HTML reports under `reports/mutation/`:
`reverse-tools` and `reverse-tools-semantics`. Required CI retains focused
coverage; the sweep retains each configuration's JSON/HTML reports separately.
Both retain available diagnostics for seven days after failure and skip uploads
on cancellation. Artifact names identify the tier or configuration and run
attempt. A successful sweep requires both jobs and their nonempty reports;
partial diagnostics and preflights do not establish complete mutation outcomes
or qualify the release revision for mutation coverage.
Fast CI starts the source-only projects after classification, independently of
build and static checks, and runs five built-dependent shards after the build.
The six test jobs still select every portable test. Full qualification keeps six
+7
View File
@@ -69,6 +69,13 @@ The observed mitmproxy 12.2.3 `SaveHar` profile can emit `postData.text: null`
for a missing request body. REA preserves that reported null, omitting the
optional field only from its validation copy for this exact producer profile;
it does not invent an empty string or body bytes.
The observed Playwright 1.58.2 profile can report a bracketed IPv6
`serverIPAddress`, forwarded from Chromium's `remoteIPAddress` through
[Playwright's HAR tracer](https://github.com/microsoft/playwright/blob/v1.58.2/packages/playwright-core/src/server/har/harTracer.ts).
REA preserves the original spelling in reported Evidence and removes brackets
only from its validation copy, only for this declared producer and a valid IPv6
literal. Invalid addresses, bracketed IPv4, ports and zone identifiers remain
validation failures. Producer declarations do not authenticate origin.
Native mitmproxy decoding requires a caller-supplied **mitmdump 12.2.3 on
Linux**. Set its absolute executable path:
@@ -72,6 +72,12 @@ checkpoint commits retain their original receipts; no history was rewritten.
## Optimization priorities and gradual migration
The [larger/non-HTTP packet checkpoint](packet-field-workflows.md) records
same-question SDK and actual Codex comparisons using existing input limits,
field/filter selection, discovery and output windows. Lower delivered JSON did
not establish stable total-token or speed gains; failed plans and negative
controls remain visible.
Agent token consumption and end-to-end task time are the primary objectives.
Memory and CPU consumption are also measured and controlled. Correctness,
Evidence provenance, caller choices and existing-user compatibility are
@@ -181,6 +181,7 @@ def main():
'inspect_android_class', 'inspect_android_method',
'inspect_firmware_regions', 'inspect_managed_artifact',
'inspect_sqlite_database', 'inspect_pcap_capture',
'inspect_packet_capture', 'inspect_packet_fields',
'inspect_managed_members', 'inspect_managed_native_boundaries',
'inspect_artifact', 'inspect_wasm_artifact',
'inspect_evm_interface', 'get_evidence_bundle', 'export_evidence_bundle',
+9 -1
View File
@@ -2,6 +2,7 @@ import { isDeepStrictEqual } from "node:util";
import { readFile, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { parseArgs } from "node:util";
import { PACKET_ARTIFACTS } from "./packet-entry.mjs";
const { values } = parseArgs({
options: {
@@ -33,6 +34,8 @@ const sameSettings = [
"provider_environment_policy_sha256",
"measure_source_sha256",
"native_workflow_source_sha256",
"packet_workflow_source_sha256",
"packet_oracle_source_sha256",
"package_lock_sha256",
"node_version",
"sdk_client_version",
@@ -76,6 +79,7 @@ for (const [side, answer] of [
// coverage. Repeated equivalent deliveries must also match every observation
// field except the receipt and parent IDs, which identify their own invocations.
if (
Object.hasOwn(PACKET_ARTIFACTS, left.artifact) ||
["wireshark-http.cap", "wireshark-http_redirects.pcapng"].includes(
left.artifact,
)
@@ -94,7 +98,11 @@ if (
path: ["delivery_observations"],
value: answer.delivery_observations,
});
if (left.delivery === right.delivery) {
if (
left.delivery === right.delivery &&
(!Object.hasOwn(PACKET_ARTIFACTS, left.artifact) ||
left.packet_selection === right.packet_selection)
) {
answer.delivery_observations = answer.delivery_observations.map(
(observation) =>
Object.fromEntries(
@@ -291,6 +291,58 @@
"upstream_expected_digest": null,
"derivation": "Unmodified capture published by official Wireshark wiki. Local digest pins downloaded bytes; no independently published upstream digest is claimed."
}
},
{
"id": "wireshark-pgsql-jdbc.pcap",
"file": "pgsql-jdbc.pcap",
"bytes": 4906316,
"sha256": "6e68b8b792a0f4336092f98856abc7de8c54b55a29112854d28d676f9093c16b",
"provenance": {
"project": "Wireshark public sample captures",
"source_page": "https://wiki.wireshark.org/SampleCaptures",
"url": "https://wiki.wireshark.org/uploads/__moin_import__/attachments/SampleCaptures/pgsql-jdbc.pcap.gz",
"resolved_url": "https://gitlab.com/wireshark/wireshark/-/wikis/uploads/__moin_import__/attachments/SampleCaptures/pgsql-jdbc.pcap.gz",
"upstream_expected_digest": null,
"license": "not stated in inspected upstream catalogue",
"derivation": "Unmodified official saved sample; decompressed gzip where indicated. Local digest pins acquired bytes, not an upstream-published digest.",
"compressed_source": {
"name": "pgsql-jdbc.pcap.gz",
"bytes": 590093,
"sha256": "15b51bb4cc197753c1ac6b7dc3cf5697f22a81e3bb5d2e1d24d15de469c3a06b"
}
}
},
{
"id": "wireshark-dns.cap",
"file": "dns.cap",
"bytes": 4338,
"sha256": "041eeb6f98bb398f1ee8b09651b5b5a84f6a62639f95bf226f9e7b77355d9f28",
"provenance": {
"project": "Wireshark public sample captures",
"source_page": "https://wiki.wireshark.org/SampleCaptures",
"url": "https://wiki.wireshark.org/uploads/__moin_import__/attachments/SampleCaptures/dns.cap",
"resolved_url": "https://gitlab.com/wireshark/wireshark/-/wikis/uploads/__moin_import__/attachments/SampleCaptures/dns.cap",
"upstream_expected_digest": null,
"license": "not stated in inspected upstream catalogue",
"derivation": "Unmodified official saved sample; decompressed gzip where indicated. Local digest pins acquired bytes, not an upstream-published digest.",
"compressed_source": null
}
},
{
"id": "wireshark-dhcp.pcap",
"file": "dhcp.pcap",
"bytes": 1400,
"sha256": "2471b5420bdac826eecf8f61a2bbb4a3eb20dbfab7c02ff2be502f349f368214",
"provenance": {
"project": "Wireshark public sample captures",
"source_page": "https://wiki.wireshark.org/SampleCaptures",
"url": "https://wiki.wireshark.org/uploads/__moin_import__/attachments/SampleCaptures/dhcp.pcap",
"resolved_url": "https://gitlab.com/wireshark/wireshark/-/wikis/uploads/__moin_import__/attachments/SampleCaptures/dhcp.pcap",
"upstream_expected_digest": null,
"license": "not stated in inspected upstream catalogue",
"derivation": "Unmodified official saved sample; decompressed gzip where indicated. Local digest pins acquired bytes, not an upstream-published digest.",
"compressed_source": null
}
}
]
}
@@ -12,6 +12,7 @@ import {
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { Ajv2020 } from "ajv/dist/2020.js";
import { PACKET_ARTIFACTS, measurePacketEntry } from "./packet-entry.mjs";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const { values } = parseArgs({
@@ -29,6 +30,7 @@ const { values } = parseArgs({
"managed-question": { type: "string", default: "inventory" },
schemas: { type: "string", default: "default" },
"sqlite-selection": { type: "string", default: "all" },
"packet-selection": { type: "string", default: "baseline" },
},
});
if (
@@ -45,6 +47,15 @@ if (
!["inventory", "selected"].includes(values["javascript-question"]) ||
!["default", "selected"].includes(values.schemas) ||
!["all", "columns"].includes(values["sqlite-selection"]) ||
!["baseline", "focused", "discovery", "pages"].includes(
values["packet-selection"],
) ||
(!Object.hasOwn(PACKET_ARTIFACTS, values.artifact) &&
values["packet-selection"] !== "baseline") ||
(Object.hasOwn(PACKET_ARTIFACTS, values.artifact) &&
values.delivery !== "complete") ||
(values["packet-selection"] === "pages" &&
values.artifact !== "wireshark-pgsql-jdbc.pcap") ||
(values.artifact !== "qgis-world-map.gpkg" &&
values["sqlite-selection"] !== "all") ||
(values.artifact === "qgis-world-map.gpkg" &&
@@ -142,6 +153,12 @@ const fingerprints = {
native_workflow_source_sha256: await fileSha(
fileURLToPath(new URL("./native-entry.mjs", import.meta.url)),
),
packet_workflow_source_sha256: await fileSha(
fileURLToPath(new URL("./packet-entry.mjs", import.meta.url)),
),
packet_oracle_source_sha256: await fileSha(
fileURLToPath(new URL("./packet-oracle.py", import.meta.url)),
),
runtime_files_sha256: sha(JSON.stringify(runtimeFiles)),
runtime_dispatcher_sha256: await fileSha(
resolve(values["runtime-root"], "scripts/rea.mjs"),
@@ -722,6 +739,12 @@ const inspectPcapTarget = async () => {
};
const workflows = {
...Object.fromEntries(
Object.keys(PACKET_ARTIFACTS).map((artifact) => [
artifact,
() => measurePacketEntry({ values, call, inputSha, catalogDefinitions }),
]),
),
"wireshark-http.cap": inspectPcapTarget,
"wireshark-http_redirects.pcapng": inspectPcapTarget,
"qgis-world-map.gpkg": async () => {
@@ -1160,6 +1183,9 @@ const report = {
values.artifact === "qgis-world-map.gpkg"
? values["sqlite-selection"]
: null,
packet_selection: Object.hasOwn(PACKET_ARTIFACTS, values.artifact)
? values["packet-selection"]
: null,
provider_settings_sha256: sha(JSON.stringify(selectedProviders)),
provider_search_path_sha256: sha(
selectedProviders.PATH ?? process.env.PATH ?? "",
@@ -0,0 +1,316 @@
import { isDeepStrictEqual } from "node:util";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { fileURLToPath } from "node:url";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
export const verifyPacketPage = (first, next, oracle) => {
if (
first.artifact.sha256 !== oracle.sha256 ||
next.artifact.sha256 !== oracle.sha256 ||
first.output_window.matching_packets !== oracle.matching_packets ||
next.output_window.matching_packets !== oracle.matching_packets ||
!isDeepStrictEqual(
packetFacts("postgres", first).rows,
oracle.rows.slice(0, 12),
) ||
!isDeepStrictEqual(
packetFacts("postgres", next).rows,
oracle.rows.slice(12, 24),
)
)
throw new Error(
"Packet pages differ from independently parsed original matches",
);
};
export const PACKET_ARTIFACTS = {
"wireshark-pgsql-jdbc.pcap": {
kind: "postgres",
fields: ["tcp.srcport", "tcp.dstport", "tcp.flags"],
extra: ["tcp.len"],
protocol: "tcp",
query: "tcp.flags",
discovered: "tcp.flags",
filter: "tcp.port == 5432",
},
"wireshark-dns.cap": {
kind: "dns",
fields: [
"dns.id",
"dns.flags.response",
"dns.flags.rcode",
"dns.qry.name",
"dns.qry.type",
],
extra: ["dns.resp.name", "dns.resp.ttl", "udp.srcport", "udp.dstport"],
protocol: "dns",
query: "dns.flags.rcode",
discovered: "dns.flags.rcode",
filter: "dns.flags.response == 1 && dns.flags.rcode != 0",
},
"wireshark-dhcp.pcap": {
kind: "dhcp",
fields: [
"dhcp.id",
"dhcp.option.dhcp",
"dhcp.ip.your",
"dhcp.option.dhcp_server_id",
],
extra: [
"dhcp.type",
"dhcp.ip.client",
"dhcp.hw.mac_addr",
"udp.srcport",
"udp.dstport",
],
protocol: "dhcp",
query: "Option 53:",
discovered: "dhcp.option.dhcp",
filter: "dhcp",
},
};
export const packetRequest = (artifact, path, selection) => {
const descriptor = PACKET_ARTIFACTS[artifact];
if (!descriptor) throw new Error("Unknown pinned packet question");
const baseline = selection === "baseline";
return {
path,
fields: baseline
? [...descriptor.fields, ...descriptor.extra]
: descriptor.fields,
display_filter:
descriptor.kind === "postgres" && baseline
? `${descriptor.filter} && frame.number <= 2000`
: descriptor.kind === "dns" && baseline
? "dns"
: descriptor.filter,
...(descriptor.kind === "postgres" && !baseline
? { packet_limit: 2000, match_limit: 12 }
: {}),
};
};
const number = (row, field) => {
const value = row[field];
if (
!Array.isArray(value) ||
value.length !== 1 ||
!/^(?:[0-9]+|0x[0-9a-f]+)$/iu.test(value[0])
)
throw new Error(`Expected one recorded integer for ${field}`);
const parsed = Number(value[0]);
if (!Number.isSafeInteger(parsed))
throw new Error(`Unsafe recorded integer for ${field}`);
return parsed;
};
export const packetFacts = (kind, report) => {
let packets = report.packets;
if (kind === "postgres") packets = packets.slice(0, 12);
if (kind === "dns")
packets = packets.filter(
(row) =>
number(row, "dns.flags.response") === 1 &&
number(row, "dns.flags.rcode") !== 0,
);
return {
kind,
matching_packets:
kind === "dns" ? packets.length : report.output_window.matching_packets,
rows: packets.map((row) => {
const frame = number(row, "frame.number");
if (kind === "postgres")
return {
frame,
source_port: number(row, "tcp.srcport"),
destination_port: number(row, "tcp.dstport"),
flags: number(row, "tcp.flags"),
};
if (kind === "dns") {
const names = row["dns.qry.name"];
const types = row["dns.qry.type"];
if (
!Array.isArray(names) ||
names.length === 0 ||
!Array.isArray(types) ||
types.length !== names.length
)
throw new Error(
"The fixed DNS question requires recorded names and types; missing fields are unknown",
);
return {
frame,
id: number(row, "dns.id"),
rcode: number(row, "dns.flags.rcode"),
names,
types: types.map((value) => number({ type: [value] }, "type")),
};
}
const server = row["dhcp.option.dhcp_server_id"];
const address = row["dhcp.ip.your"];
if (
!Array.isArray(address) ||
address.length !== 1 ||
(server !== undefined &&
(!Array.isArray(server) || server.length !== 1))
)
throw new Error("Expected recorded DHCP address scalar");
return {
frame,
id: number(row, "dhcp.id"),
message_type: number(row, "dhcp.option.dhcp"),
your_address: address[0],
...(server === undefined ? {} : { server_identifier: server[0] }),
};
}),
};
};
export const measurePacketEntry = async ({
values,
call,
inputSha,
catalogDefinitions,
}) => {
const descriptor = PACKET_ARTIFACTS[values.artifact];
const { parseEvidence } = await import(
pathToFileURL(resolve(values["runtime-root"], "dist/domain/evidence.js"))
.href
);
const { inspectPacketCaptureInputSchema, packetCaptureSchema } = await import(
pathToFileURL(
resolve(values["runtime-root"], "dist/domain/captures/packetCapture.js"),
).href
);
const { inspectPacketFieldsInputSchema, packetFieldsCatalogSchema } =
await import(
pathToFileURL(
resolve(values["runtime-root"], "dist/domain/captures/packetFields.js"),
).href
);
const observations = [];
const selection = values["packet-selection"];
const checked = (evidence, operation, input, schema) => {
parseEvidence(evidence);
const report = schema.parse(evidence.normalized_result);
if (
evidence.operation !== operation ||
!isDeepStrictEqual(evidence.parameters, input) ||
!isDeepStrictEqual(report.selection, input)
)
throw new Error(
"Packet Evidence operation or choices differs from the request",
);
if (
operation === "inspect_packet_capture" &&
(evidence.subject?.local_path !== values.path ||
evidence.subject?.digest?.sha256 !== inputSha ||
report.artifact.path !== values.path ||
report.artifact.sha256 !== inputSha)
)
throw new Error("Packet Evidence identifies different capture bytes");
if (
operation === "inspect_packet_fields" &&
(evidence.subject?.local_path !== report.tool.path ||
evidence.subject?.digest?.sha256 !== report.tool.sha256)
)
throw new Error("Field discovery identifies a different executable");
observations.push({
evidence_id: evidence.evidence_id,
operation,
selection: report.selection,
output_window: report.output_window,
read_scope: report.read_scope ?? null,
limitations: evidence.limitations,
});
return report;
};
if (selection === "discovery") {
const input = inspectPacketFieldsInputSchema.parse({
kind: "fields",
protocol: descriptor.protocol,
query: descriptor.query,
limit: 1,
});
const found = checked(
await call("inspect_packet_fields", input),
"inspect_packet_fields",
input,
packetFieldsCatalogSchema,
);
if (
!found.records.some((row) => row.abbreviation === descriptor.discovered)
)
throw new Error(
"Selected installation does not advertise the required protocol field",
);
}
const input = inspectPacketCaptureInputSchema.parse(
packetRequest(values.artifact, values.path, selection),
);
for (const name of [
"inspect_packet_capture",
...(selection === "discovery" ? ["inspect_packet_fields"] : []),
])
if (!catalogDefinitions.has(name))
throw new Error(`Missing live packet contract ${name}`);
const report = checked(
await call("inspect_packet_capture", input),
"inspect_packet_capture",
input,
packetCaptureSchema,
);
const facts = packetFacts(descriptor.kind, report);
if (selection === "pages") {
const { stdout } = await promisify(execFile)(
"python3",
[
fileURLToPath(new URL("./packet-oracle.py", import.meta.url)),
"postgres",
values.path,
"--row-limit",
"24",
],
{ timeout: 30_000, maxBuffer: 128 * 1024 },
);
const oracle = JSON.parse(stdout);
const nextInput = inspectPacketCaptureInputSchema.parse({
...input,
match_offset: 12,
});
const next = checked(
await call("inspect_packet_capture", nextInput),
"inspect_packet_capture",
nextInput,
packetCaptureSchema,
);
const repeated = checked(
await call("inspect_packet_capture", input),
"inspect_packet_capture",
input,
packetCaptureSchema,
);
verifyPacketPage(report, next, oracle);
if (
!isDeepStrictEqual(report.packets, repeated.packets) ||
!isDeepStrictEqual(report.output_window, repeated.output_window) ||
next.artifact.sha256 !== report.artifact.sha256 ||
next.output_window.matching_packets !==
report.output_window.matching_packets ||
Number(next.packets[0]?.["frame.number"]?.[0]) <=
Number(report.packets.at(-1)?.["frame.number"]?.[0])
)
throw new Error(
"Packet page order, digest or repeated observations changed",
);
}
return {
artifact_sha256: inputSha,
facts,
delivery_observations: observations,
evidence_ids: [...new Set(observations.map((row) => row.evidence_id))],
};
};
@@ -0,0 +1,226 @@
# Larger and non-HTTP packet investigation costs
This closes the bounded experiment in [#3130](https://github.com/morluto/rea/issues/3130).
Existing field, filter and input/output-window controls substantially reduce
delivered JSON for the selected questions. Actual Codex total input and elapsed
time are less consistent. Requiring field discovery for a known field adds cost.
These results support documenting caller-selected controls, while retaining the
current API, defaults and uncached provider behavior.
## Runtime and public corpus
Measurements use immutable main
`ec67aebed02dbd20fa5c7703fcd183ad0b1c343c`, Node 24.18.0, the locked MCP client and
server, Linux, TShark/capinfos 4.0.17 and util-linux prlimit. The SDK captures live
advertised contracts and passes selected definitions to actual public MCP calls.
Runtime-file, dispatcher, lockfile, experiment-source and explicit provider
configuration digests remain with the private trial records. Subsequent main
integration checks do not relabel these measurements.
Three additional saved recordings come from the
[official Wireshark catalogue](https://wiki.wireshark.org/SampleCaptures).
`corpus.json` pins their acquisition URLs, exact bytes and SHA-256 values.
The PostgreSQL recording is described upstream as a JDBC test session; it is
not a representative production workload. Capture-specific licenses were not
stated in the inspected catalogue. No captures or raw trial records are
redistributed in Git.
| Recording | Acquired/analyzed size | Recorded frames | Fixed question |
| -------------------- | --------------------------------------------: | --------------: | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `pgsql-jdbc.pcap.gz` | 590,093 compressed / 4,906,316 expanded bytes | 18,472 | Within original frames 1–2,000, report the first twelve TCP packets using port 5432, their source/destination ports and flags, and the matching count. |
| `dns.cap` | 4,338 bytes | 38 | Report nonzero-RCODE DNS responses, transaction IDs and question names/types. |
| `dhcp.pcap` | 1,400 bytes | 4 | Report message types, transaction IDs, offered address and present server identifiers in frame order. |
The larger application recording and two small non-HTTP controls expand the
question/protocol coverage. Large DNS/DHCP, PCAPNG non-HTTP, USB, Bluetooth,
Windows and macOS provider performance remain unverified.
`packet-oracle.py` independently reads classic PCAP, Ethernet/IPv4 and selected
TCP/UDP headers. It does not consume TShark output. Its narrow supported scope
fails on truncation, IP fragmentation, DNS compression cycles, duplicate DHCP
options or DHCP overload. TCP header facts follow
[RFC 9293](https://www.rfc-editor.org/rfc/rfc9293.html), DNS names/header fields
[RFC 1035](https://www.rfc-editor.org/rfc/rfc1035.html), and DHCP options
[RFC 2132](https://www.rfc-editor.org/rfc/rfc2132.html).
SQL interpretation, TCP reassembly and network completeness are outside the
oracle. The question and oracle were fixed before measured cohorts.
The oracle establishes 1,337 selected PostgreSQL TCP packets in the input prefix
and six DNS error responses. DHCP records contain Discover/Offer with transaction
ID 15645 and Request/ACK with ID 15646. An absent server identifier stays omitted;
the recording does not establish a single transaction containing all four steps.
## Equivalent questions and SDK measurements
Each arm uses fresh actual MCP sessions; heavy trials run sequentially.
Two interleaved repetitions cover each arm and each recording. Discovery, catalog,
RPC, schema checking, encoding and cleanup costs are retained. JSON bytes describe
serialized SDK replies, not raw transport bytes or model tokens.
The PostgreSQL baseline selects `tcp.srcport`, `tcp.dstport`, `tcp.flags` and
`tcp.len`, retains the default fifty-row output window and reads to EOF with
`tcp.port == 5432 && frame.number <= 2000`. The focused arm selects the three
required fields, uses `packet_limit: 2000`, filter `tcp.port == 5432` and
`match_limit: 12`. Both establish the same requested prefix facts and count.
Their source read scopes differ explicitly: to EOF versus an input limit.
Neither scope establishes whole-network completeness.
DNS focuses the nine-field baseline on five requested fields and the error-response
filter. DHCP selects four required fields from the nine-field baseline; all four
records are still read. The discovery arm adds one narrow installed-field probe
before the focused request. It tests the cost of validating an already specified
field, rather than proving a benefit for a previously unknown field or guessing
that an installed registration appears in traffic.
| Question | Baseline reply bytes | Focused reply bytes | Focused change | Discovery + focused bytes | Discovery change |
| ----------------- | -------------------: | ------------------: | -------------: | ------------------------: | ---------------: |
| PostgreSQL prefix | 23,054 | 13,304 | −42.29% | 21,096 | −8.49% |
| DNS errors | 29,002 | 13,424 | −53.71% | 21,226 | −26.81% |
| DHCP sequence | 13,770 | 12,020 | −12.71% | 19,840 | +44.08% |
Both repetitions produce the same reply size in each cell and pass the independent
fact comparison. Authentication uses the selected runtime's `parseEvidence` and
named input/output schemas, checking operation, normalized caller choices,
artifact or executable identity, original frame order and omission semantics.
| Question | Baseline tool-call time, ms | Focused tool-call time, ms | Discovery + focused tool-call time, ms |
| ----------------- | --------------------------- | -------------------------- | -------------------------------------- |
| PostgreSQL prefix | 1,670 / 2,255 | 1,708 / 933 | 3,549 / 2,674 |
| DNS errors | 892 / 786 | 889 / 778 | 2,506 / 2,608 |
| DHCP sequence | 763 / 759 | 755 / 731 | 2,502 / 2,502 |
Whole SDK workflow times, including connection/catalog, vary from 4.23–8.79 seconds
for these arms. The PostgreSQL focused tool time improves in one repetition and
regresses slightly in the other. Discovery consistently adds tool work. Two
repetitions are descriptive observations, not a statistical speed guarantee.
A separate first-page, next-page and repeated-first-page sequence costs
922 / 804 / 1,244 ms and returns 39,918 reply bytes in total. Each call repeats
dissection, reports the same 1,337 matching count and capture digest, and preserves
the original frame order. Identical repeated observations retain the same
content-addressed Evidence ID; distinct pages have distinct IDs. Page delivery
does not establish capture or network completeness. This narrow repeat does not
justify introducing a generic cache or weakening provider identity/cleanup rules.
The two original HTTP controls were also replayed with two SDK pairs each on the
new pinned runtime. Useful summary/prefix facts remain equivalent. For `http.cap`,
complete delivery returns 7,601 bytes and summary plus a two-URI retained page
17,540 bytes (+130.76%); for `http_redirects.pcapng`, 7,535 versus 17,240 bytes
(+128.80%). These remain negative controls. The original model observations in
[the earlier checkpoint](pcap-small-captures.md) keep their original runtime pin.
## Actual Codex CLI observations
Six actual Codex CLI 0.161.0 runs use requested alias `gpt-5.6-terra`, reasoning
`high`, a disposable configuration and the pinned REA MCP runtime. Resolved
server-side model identity is not inferred. Both tool names are enabled in every
arm, exact-name discovery guidance is identical, shell/browser/other-agent tools
are disabled and copied authentication is removed after each run.
The shared question and generated plans contain no oracle answers.
Order is baseline 1, focused 1, discovery 1, focused 2, baseline 2, discovery 2.
The rubric checks authenticated actual tool results, exact header-derived facts,
citations and the planned calls. Answer correctness and adherence to the plan
are reported separately. Limitation wording is reviewed in transcripts; a string
or string array is accepted because the prompt does not require one representation.
| Trial | Total input tokens | Cached input tokens | Output tokens | Elapsed seconds | Actual MCP calls | Facts / plan |
| ----------- | -----------------: | ------------------: | ------------: | --------------: | ---------------: | ---------------------------------- |
| Baseline 1 | 40,649 | 27,904 | 1,574 | 27.989 | 1 | pass / pass |
| Focused 1 | 36,642 | 19,968 | 943 | 38.542 | 1 | pass / pass |
| Discovery 1 | 52,802 | 33,024 | 1,089 | 44.398 | 2 | pass / pass |
| Focused 2 | 40,912 | 22,016 | 726 | 32.520 | 1 | pass / pass |
| Baseline 2 | 40,181 | 19,968 | 896 | 40.299 | 1 | pass / pass |
| Discovery 2 | 48,754 | 30,976 | 1,780 | 76.081 | 1 | pass / **fail: discovery omitted** |
The two eligible baseline/focused pairs show total-input changes of −9.86% and
+1.82%, and elapsed changes of +37.70% and −19.30%. Thus reduced SDK reply size
does not establish a stable total-token or speed improvement in these runs.
The first complete discovery comparison adds 29.90% input and 58.63% elapsed time.
The second discovery attempt is retained as a workflow failure and cannot serve
as a successful two-call discovery comparison. Its cost is not discarded or
counted as a passing trial. No model-side recovery or retries occurred in these
six attempts.
An earlier configuration preflight rejected the packet tool under Codex's
`approval_policy: never`: 36,651 input tokens, 549 output tokens, 21.603 seconds,
one rejected call and no packet observations. The experiment runner now explicitly
preapproves the two authorized passive packet tools, consistently across all arms.
That failed preflight remains separate from the corrected cohort and its cost is
reported here; CLI exit zero alone is never a factual or workflow pass.
## Resource and compatibility scope
All engine/model workloads use an owned Linux resource scope with one CPU,
3 GiB early-stop threshold, 4 GiB hard memory ceiling, no swap and a 192-task cap.
Node's heap is additionally limited to 512 MiB. The nineteen non-HTTP SDK trials
have a maximum observed payload-exit memory peak of 860,909,568 bytes; the six model
attempts peak at 940,806,144 bytes. Payload-exit counters and cleanup were verified
with the observer still alive and report no memory-limit/OOM events in that phase.
Final whole-scope counters remain unavailable, so no stronger whole-scope claim
is made. CPU usage and observed process ownership remain in each private receipt.
A full PostgreSQL probe with four selected fields and no input prefix exceeds the
existing 4 MiB producer-output ceiling even with the default fifty-row window.
It correctly fails instead of delivering partial success. The SDK error retains
large truncated diagnostics (5,790,979 serialized bytes); this separate product
concern is recorded privately for later repair, outside #3130. Paging alone cannot
prevent producer expansion. The successful prefix experiment does not prove that
the full recording can be inspected with those selections.
Only experiment sources, corpus provenance, a question template and guidance
change. Production schemas, provider limits, CLI/MCP defaults, Evidence identity
and session/resource ownership stay as before. Eight parser regressions retain
malformed/truncated-input and omission checks. Existing clients need no migration.
## Reproduction
Acquire the exact corpus outside the repository and verify the pinned hashes.
Use the pinned Node/npm toolchain, locked dependencies, `npm run build:cached` and
an external resource guard before invoking actual engines/models. Supply explicit
provider settings in a private JSON file. An example SDK arm is:
```sh
node experiments/analysis-efficiency/measure.mjs \
--runtime-root /absolute/pinned/runtime --revision PINNED_SHA \
--artifact wireshark-pgsql-jdbc.pcap --path /private/pgsql-jdbc.pcap \
--providers /private/providers.json --schemas selected \
--packet-selection focused --output /private/new-trial
```
Use `baseline`, `focused`, `discovery`, or the PostgreSQL-only `pages` diagnostic.
Run `packet-oracle.py postgres PATH` independently and compare `answer.json` facts.
Generate each model prompt with `packet-model-prompt.mjs --selection ARM`, then use
the existing `agent-trial.py` with those two enabled tools and context capture.
Run `review-packet.mjs` against the actual trial, oracle, runtime and selected arm.
Its nonzero exit includes a plan failure even when all requested facts are correct.
Retain failed attempts, actual usage, digests, coverage, cleanup and resource
receipts alongside passing observations. No raw results need to be committed.
## Review verification
The comparator now recognizes the three non-HTTP questions: observations remain
attached to each run, while different plans compare requested facts. Repeated
identical plans still compare stable observation scope. Capture grading authenticates
Evidence against the actual call and assesses plan differences separately; an
actual 24-row SDK window replay retains a factual pass and a plan failure. This
is a grading replay, not an additional model trial. The stricter header oracle
preserves all original sample facts and rejects truncated IPv4/TCP options and
unsupported Ethernet protocols. The page diagnostic now derives matches 13–24
independently before accepting the second page; skipped, reversed and changed-byte
pages are rejected. Original cohort measurements retain their recorded source
digests and runtime pin; review checks do not relabel them.
Capture grading keeps authenticated facts by Evidence ID and evaluates the cited
observations. Four replays using actual first-page and second-page SDK Evidence
verify that later or reordered pages and repeated reads preserve a correct cited
answer, while citing only the wrong page fails factual grading. Extra calls still
fail the fixed workflow plan. Regrading all six original model records preserves
their outcomes and recorded costs; these replays are not new model trials.
Failed or malformed calls remain workflow failures and never become authenticated
Evidence. Nine failure-injection and answer-shape replays using an actual successful
capture verify that later recovery preserves supported facts, while failed-only
attempts and missing or unobserved citations fail factual grading. Missing
limitations and extra answer keys fail the fixed plan without changing correct
recorded facts. Successful original-model outcomes remain unchanged.
@@ -0,0 +1,40 @@
import { readFile } from "node:fs/promises";
import { parseArgs } from "node:util";
import { packetRequest } from "./packet-entry.mjs";
const { values } = parseArgs({ options: { selection: { type: "string" } } });
if (!["baseline", "focused", "discovery"].includes(values.selection))
throw new Error("Select baseline, focused or discovery");
const question = await readFile(
new URL("./questions/packet-postgres.txt", import.meta.url),
"utf8",
);
const plan = [
...(values.selection === "discovery"
? [
{
tool: "inspect_packet_fields",
arguments: {
kind: "fields",
protocol: "tcp",
query: "tcp.flags",
limit: 1,
},
},
]
: []),
{
tool: "inspect_packet_capture",
arguments: packetRequest(
"wireshark-pgsql-jdbc.pcap",
"{{artifact_path}}",
values.selection,
),
},
];
process.stdout.write(
question +
"\nExecute this fixed public REA MCP plan in order. These are input selections, not expected answers. Preserve omitted defaults. All calls count toward the comparison.\n" +
JSON.stringify(plan, null, 2) +
"\n",
);
@@ -0,0 +1,180 @@
"""Independent, narrow oracle for the three pinned Ethernet/IPv4 PCAPs.
Reads header bytes, not TShark output. No PCAPNG, IP fragmentation or TCP
reassembly support is claimed. DNS compression follows RFC 1035; DHCP options
53/54 follow RFC 2132. Unsupported selected traffic fails rather than guessing.
"""
import argparse
import hashlib
import json
import struct
from pathlib import Path
def require(condition, reason):
if not condition:
raise ValueError(reason)
def packets(data):
require(len(data) >= 24, 'truncated PCAP header')
magic = data[:4]
require(magic in (b'\xd4\xc3\xb2\xa1', b'\xa1\xb2\xc3\xd4'),
'oracle requires microsecond classic PCAP')
endian = '<' if magic == b'\xd4\xc3\xb2\xa1' else '>'
require(struct.unpack_from(endian + 'HH', data, 4) == (2, 4),
'unsupported PCAP version')
require(struct.unpack_from(endian + 'I', data, 20)[0] == 1,
'oracle requires Ethernet')
offset, frame = 24, 0
while offset < len(data):
require(offset + 16 <= len(data), 'truncated PCAP record header')
captured, original = struct.unpack_from(endian + 'II', data, offset + 8)
offset += 16
require(captured <= original and offset + captured <= len(data),
'invalid captured packet length')
frame += 1
yield frame, data[offset:offset + captured]
offset += captured
def transport(packet):
require(len(packet) >= 14, 'truncated Ethernet header')
if packet[12:14] == b'\x08\x06':
return None # ARP cannot contain the selected TCP/UDP messages.
require(packet[12:14] == b'\x08\x00', 'unsupported Ethernet protocol')
ip = packet[14:]
require(len(ip) >= 20 and ip[0] >> 4 == 4, 'invalid IPv4 header')
header = (ip[0] & 15) * 4
total = struct.unpack_from('!H', ip, 2)[0]
require(20 <= header <= len(ip) and total >= header, 'invalid IPv4 lengths')
require(total <= len(ip), 'truncated IPv4 packet')
require(struct.unpack_from('!H', ip, 6)[0] & 0x3fff == 0,
'fragmentation is outside oracle scope')
return ip[9], ip[header:min(total, len(ip))]
def dns_name(message, offset):
labels, visited, end = [], set(), None
while True:
require(offset < len(message) and offset not in visited,
'invalid DNS name or compression cycle')
visited.add(offset)
size = message[offset]
if size & 0xc0 == 0xc0:
require(offset + 1 < len(message), 'truncated DNS compression pointer')
if end is None:
end = offset + 2
offset = ((size & 0x3f) << 8) | message[offset + 1]
continue
require(size & 0xc0 == 0 and offset + 1 + size <= len(message),
'invalid DNS label')
offset += 1
if size == 0:
return '.'.join(labels), end if end is not None else offset
labels.append(message[offset:offset + size].decode('ascii'))
require(sum(len(label) + 1 for label in labels) <= 255,
'DNS name exceeds protocol length')
offset += size
def dhcp_options(payload):
require(len(payload) >= 240 and payload[236:240] == b'\x63\x82\x53\x63',
'invalid DHCP fixed header or cookie')
options, offset = {}, 240
while offset < len(payload):
kind = payload[offset]
offset += 1
if kind == 255:
return options
if kind == 0:
continue
require(offset < len(payload), 'missing DHCP option length')
size = payload[offset]
offset += 1
require(offset + size <= len(payload), 'truncated DHCP option')
require(kind not in options, 'duplicate DHCP options outside oracle scope')
options[kind] = payload[offset:offset + size]
offset += size
raise ValueError('DHCP end option missing')
def analyze(data, kind, row_limit=12):
rows, matching, count = [], 0, 0
for frame, packet in packets(data):
count = frame
decoded = transport(packet)
if decoded is None:
continue
protocol, segment = decoded
if kind == 'postgres' and protocol == 6 and frame <= 2000:
require(len(segment) >= 20, 'truncated TCP header')
tcp_header = (segment[12] >> 4) * 4
require(20 <= tcp_header <= len(segment), 'invalid or truncated TCP header/options')
source, destination = struct.unpack_from('!HH', segment)
if 5432 not in (source, destination):
continue
matching += 1
if len(rows) < row_limit:
rows.append({'frame': frame, 'source_port': source,
'destination_port': destination,
'flags': struct.unpack_from('!H', segment, 12)[0] & 0xfff})
elif kind in ('dns', 'dhcp') and protocol == 17:
require(len(segment) >= 8, 'truncated UDP header')
source, destination, length = struct.unpack_from('!HHH', segment)
selected = 53 in (source, destination) if kind == 'dns' else (
source, destination) in ((67, 68), (68, 67))
if not selected:
continue
require(8 <= length <= len(segment), 'truncated selected UDP message')
payload = segment[8:length]
if kind == 'dns':
require(len(payload) >= 12, 'truncated DNS header')
identifier, flags, questions = struct.unpack_from('!HHH', payload)
if not flags & 0x8000 or flags & 15 == 0:
continue
matching += 1
offset, names, types = 12, [], []
for _ in range(questions):
name, offset = dns_name(payload, offset)
require(offset + 4 <= len(payload), 'truncated DNS question')
types.append(struct.unpack_from('!H', payload, offset)[0])
names.append(name)
offset += 4
rows.append({'frame': frame, 'id': identifier, 'rcode': flags & 15,
'names': names, 'types': types})
else:
options = dhcp_options(payload)
require(53 in options and len(options[53]) == 1,
'missing DHCP message type')
server = options.get(54)
require(server is None or len(server) == 4, 'invalid DHCP server ID')
require(52 not in options, 'DHCP overload outside oracle scope')
matching += 1
row = {'frame': frame, 'id': struct.unpack_from('!I', payload, 4)[0],
'message_type': options[53][0],
'your_address': '.'.join(map(str, payload[16:20]))}
if server is not None:
row['server_identifier'] = '.'.join(map(str, server))
rows.append(row)
return {'kind': kind, 'capture_frames': count, 'matching_packets': matching,
'question_input_frames': min(2000, count) if kind == 'postgres' else count,
'rows': rows, 'scope': 'selected recorded headers; no network completeness claim'}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('kind', choices=['postgres', 'dns', 'dhcp'])
parser.add_argument('path', type=Path)
parser.add_argument('--row-limit', type=int, choices=[12, 24], default=12,
help='PostgreSQL rows; 24 independently checks the second page')
args = parser.parse_args()
with args.path.open('rb') as source:
data = source.read(32 * 1024 * 1024 + 1)
require(len(data) <= 32 * 1024 * 1024, 'oracle input budget exceeded')
print(json.dumps({'sha256': hashlib.sha256(data).hexdigest(),
**analyze(data, args.kind, args.row_limit)}, indent=2))
if __name__ == '__main__':
main()
@@ -0,0 +1,3 @@
Inspect the selected saved PostgreSQL capture {{artifact_path}} with REA. Within original capture frames 1 through 2000, return the first 12 TCP packets with source or destination port 5432 in original frame order. For each return integer frame, source_port, destination_port and flags (the numeric TCP flags word, not prose). Also report the matching packet count within this 2000-frame question scope.
Do not claim whole-capture or network completeness. Return a single JSON object with exactly kind ("postgres"), matching_packets, rows, evidence_ids and limitations. Use actual successful REA Evidence IDs. Include a concise limitation explaining the recorded input scope; no extra claims or tool calls are needed.
@@ -0,0 +1,247 @@
import { readFile, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { isDeepStrictEqual, parseArgs } from "node:util";
import { packetFacts, packetRequest } from "./packet-entry.mjs";
const { values } = parseArgs({
options: {
trial: { type: "string" },
oracle: { type: "string" },
"runtime-root": { type: "string" },
selection: { type: "string" },
},
});
if (
!values.trial ||
!values.oracle ||
!values["runtime-root"] ||
!["baseline", "focused", "discovery"].includes(values.selection)
)
throw new Error(
"Use --trial DIR --oracle JSON --runtime-root DIR --selection baseline|focused|discovery",
);
const root = values["runtime-root"];
const { parseEvidence } = await import(
pathToFileURL(resolve(root, "dist/domain/evidence.js")).href
);
const { inspectPacketCaptureInputSchema, packetCaptureSchema } = await import(
pathToFileURL(resolve(root, "dist/domain/captures/packetCapture.js")).href
);
const { inspectPacketFieldsInputSchema, packetFieldsCatalogSchema } =
await import(
pathToFileURL(resolve(root, "dist/domain/captures/packetFields.js")).href
);
const oracle = JSON.parse(await readFile(values.oracle, "utf8"));
const settings = JSON.parse(
await readFile(resolve(values.trial, "trial-settings.json"), "utf8"),
);
const summary = JSON.parse(
await readFile(resolve(values.trial, "summary.json"), "utf8"),
);
const expected = Object.fromEntries(
["kind", "matching_packets", "rows"].map((key) => [key, oracle[key]]),
);
const calls = (await readFile(resolve(values.trial, "events.jsonl"), "utf8"))
.split("\n")
.filter(Boolean)
.map(JSON.parse)
.filter(
(event) =>
event.type === "item.completed" && event.item?.type === "mcp_tool_call",
)
.map((event) => event.item);
const reasons = [];
const workflowReasons = [];
let answer;
const captures = new Map();
try {
if (oracle.kind !== "postgres")
throw new Error("This model rubric is the fixed PostgreSQL question only");
const expectedCalls =
values.selection === "discovery"
? ["inspect_packet_fields", "inspect_packet_capture"]
: ["inspect_packet_capture"];
if (
!isDeepStrictEqual(
calls.map((call) => call.tool),
expectedCalls,
) ||
calls.some(
(call) =>
call.server !== "rea" || call.status !== "completed" || call.error,
)
)
workflowReasons.push(
"Completed public REA call sequence differs from the fixed plan",
);
const ids = new Set();
const conflictingIds = new Set();
for (const call of calls) {
if (call.server !== "rea" || call.status !== "completed" || call.error)
continue;
try {
const result = call.result;
if (result?.isError)
throw new Error("Tool failure is not observed packet evidence");
const text =
result?.content?.filter((item) => item.type === "text") ?? [];
if (!result?.structuredContent && text.length !== 1)
throw new Error("No unambiguous captured Evidence envelope");
const evidence = result.structuredContent ?? JSON.parse(text[0].text);
parseEvidence(evidence);
if (conflictingIds.has(evidence.evidence_id))
throw new Error("Previously conflicting Evidence identity");
if (call.tool === "inspect_packet_fields") {
const input = inspectPacketFieldsInputSchema.parse({
kind: "fields",
protocol: "tcp",
query: "tcp.flags",
limit: 1,
});
const report = packetFieldsCatalogSchema.parse(
evidence.normalized_result,
);
if (
!isDeepStrictEqual(
inspectPacketFieldsInputSchema.parse(call.arguments),
input,
) ||
!isDeepStrictEqual(evidence.parameters, input) ||
!isDeepStrictEqual(report.selection, input) ||
evidence.operation !== call.tool ||
!report.records.some((row) => row.abbreviation === "tcp.flags") ||
evidence.subject?.local_path !== report.tool.path ||
evidence.subject?.digest?.sha256 !== report.tool.sha256
)
throw new Error(
"Discovery Evidence does not establish the requested installed field",
);
} else if (call.tool === "inspect_packet_capture") {
const planned = inspectPacketCaptureInputSchema.parse(
packetRequest(
"wireshark-pgsql-jdbc.pcap",
settings.selected_artifact_path,
values.selection,
),
);
const input = inspectPacketCaptureInputSchema.parse(call.arguments);
if (!isDeepStrictEqual(input, planned))
workflowReasons.push("Capture selection differs from the fixed plan");
const report = packetCaptureSchema.parse(evidence.normalized_result);
if (
!isDeepStrictEqual(evidence.parameters, input) ||
!isDeepStrictEqual(report.selection, input) ||
evidence.operation !== call.tool ||
evidence.subject?.local_path !== input.path ||
evidence.subject?.digest?.sha256 !== oracle.sha256 ||
report.artifact.path !== input.path ||
report.artifact.sha256 !== oracle.sha256
)
throw new Error(
"Capture Evidence differs from the fixed input/byte identity",
);
const observed = packetFacts("postgres", report);
if (
captures.has(evidence.evidence_id) &&
!isDeepStrictEqual(captures.get(evidence.evidence_id), observed)
) {
captures.delete(evidence.evidence_id);
ids.delete(evidence.evidence_id);
conflictingIds.add(evidence.evidence_id);
throw new Error("Repeated Evidence identity has inconsistent facts");
}
captures.set(evidence.evidence_id, observed);
} else {
throw new Error("Call is not a packet observation tool");
}
ids.add(evidence.evidence_id);
} catch (error) {
workflowReasons.push(
`Call ${call.tool} did not provide authenticated packet observations: ${String(error)}`,
);
}
}
answer = JSON.parse(
(await readFile(resolve(values.trial, "answer.txt"), "utf8")).trim(),
);
if (
!isDeepStrictEqual(
Object.keys(answer).sort(),
[
"kind",
"matching_packets",
"rows",
"evidence_ids",
"limitations",
].sort(),
)
)
workflowReasons.push("Final answer shape differs from the fixed plan");
if (
!isDeepStrictEqual(
Object.fromEntries(
Object.keys(expected).map((key) => [key, answer[key]]),
),
expected,
)
)
throw new Error("Final answer is not the exact requested recorded facts");
if (
!Array.isArray(answer.evidence_ids) ||
answer.evidence_ids.some((id) => !ids.has(id)) ||
new Set(answer.evidence_ids).size !== answer.evidence_ids.length
)
throw new Error("Final citations lack the authenticated packet Evidence");
const supportingFacts = answer.evidence_ids
.map((id) => captures.get(id))
.find((observed) => isDeepStrictEqual(observed, expected));
if (!supportingFacts)
throw new Error(
"Cited observations differ from independent raw header oracle",
);
const limitations =
typeof answer.limitations === "string"
? [answer.limitations]
: answer.limitations;
if (
!Array.isArray(limitations) ||
limitations.length === 0 ||
limitations.some((item) => typeof item !== "string" || !item.trim())
)
workflowReasons.push("Final answer omits investigation limitations");
if (
summary.exit_code !== 0 ||
summary.timeout ||
summary.selected_artifact_bytes_unchanged !== true ||
settings.selected_artifact_sha256_before !== oracle.sha256 ||
summary.selected_artifact_sha256_after !== oracle.sha256
)
workflowReasons.push(
"Trial did not complete with the selected artifact unchanged",
);
} catch (error) {
reasons.push(String(error));
}
const review = {
factual_correctness: reasons.length ? "failed" : "passed",
scope:
"Fixed first-2000-frame PostgreSQL question; authenticated tool observations and independent raw header oracle",
workflow_correctness:
reasons.length || workflowReasons.length ? "failed" : "passed",
reasons,
workflow_reasons: workflowReasons,
completed_calls: calls.length,
wall_seconds: summary.wall_seconds,
usage: summary.usage,
selected_artifact_sha256: oracle.sha256,
limitation_prose:
"Presence checked; wording requires transcript review, not heuristic factual certification",
};
await writeFile(
resolve(values.trial, "packet-review.json"),
JSON.stringify(review, null, 2) + "\n",
);
console.log(JSON.stringify(review));
if (reasons.length || workflowReasons.length) process.exitCode = 1;
@@ -0,0 +1,67 @@
import importlib.util
import struct
import unittest
from pathlib import Path
spec = importlib.util.spec_from_file_location('packet_oracle', Path(__file__).with_name('packet-oracle.py'))
oracle = importlib.util.module_from_spec(spec)
spec.loader.exec_module(oracle)
class PacketOracleTest(unittest.TestCase):
def test_compression_pointer_preserves_question_cursor(self):
message = b'\x03www\x07example\x03com\0' + b'\xc0\0' + b'\0\x01\0\x01'
self.assertEqual(oracle.dns_name(message, 17), ('www.example.com', 19))
def test_compression_cycle_and_missing_pointer_byte_fail(self):
for message in (b'\xc0\0', b'\xc0', b'\xc0\x02\xc0\0'):
with self.subTest(message=message), self.assertRaises(ValueError):
oracle.dns_name(message, 0)
def test_truncated_pcap_record_is_not_exhaustion(self):
header = struct.pack('<IHHIIII', 0xa1b2c3d4, 2, 4, 0, 0, 65535, 1)
with self.assertRaises(ValueError):
list(oracle.packets(header + b'\0'))
record = struct.pack('<IIII', 0, 0, 20, 20)
with self.assertRaises(ValueError):
list(oracle.packets(header + record + b'\0' * 19))
def test_missing_dhcp_server_id_stays_omitted(self):
payload = b'\0' * 236 + b'\x63\x82\x53\x63\x35\x01\x01\xff'
self.assertEqual(oracle.dhcp_options(payload), {53: b'\x01'})
def test_dhcp_duplicates_truncation_and_missing_end_fail(self):
prefix = b'\0' * 236 + b'\x63\x82\x53\x63'
for suffix in (b'\x35\x01\x01\x35\x01\x02\xff', b'\x36\x04\0', b'\x35\x01\x01'):
with self.subTest(suffix=suffix), self.assertRaises(ValueError):
oracle.dhcp_options(prefix + suffix)
def test_fragmented_selected_traffic_cannot_pass_header_oracle(self):
ip = bytearray(20)
ip[0], ip[9] = 0x45, 17
struct.pack_into('!H', ip, 2, 20)
struct.pack_into('!H', ip, 6, 0x2000)
with self.assertRaises(ValueError):
oracle.transport(b'\0' * 12 + b'\x08\0' + ip)
def test_declared_ip_length_and_tcp_options_cannot_be_truncated(self):
ip = bytearray(40)
ip[0], ip[9], ip[32] = 0x45, 6, 0xf0
struct.pack_into('!H', ip, 2, 40)
struct.pack_into('!HH', ip, 20, 12345, 5432)
packet = b'\0' * 12 + b'\x08\0' + ip
header = struct.pack('<IHHIIII', 0xa1b2c3d4, 2, 4, 0, 0, 65535, 1)
record = struct.pack('<IIII', 0, 0, len(packet), len(packet))
with self.assertRaisesRegex(ValueError, 'TCP header'):
oracle.analyze(header + record + packet, 'postgres')
struct.pack_into('!H', ip, 2, 60)
with self.assertRaisesRegex(ValueError, 'truncated IPv4'):
oracle.transport(b'\0' * 12 + b'\x08\0' + ip)
def test_unknown_link_protocol_does_not_prove_tcp_absence(self):
with self.assertRaisesRegex(ValueError, 'unsupported Ethernet'):
oracle.transport(b'\0' * 12 + b'\x86\xdd' + b'\0' * 40)
if __name__ == '__main__':
unittest.main()
+4 -4
View File
@@ -2905,11 +2905,11 @@ pure-rand@8.4.2 | development | MIT
Package: https://www.npmjs.com/package/pure-rand/v/8.4.2
Archive: https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz
Integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==
qs@6.15.1 | development | BSD-3-Clause
qs@6.16.0 | development | BSD-3-Clause
Lockfile path: node_modules/qs
Package: https://www.npmjs.com/package/qs/v/6.15.1
Archive: https://registry.npmjs.org/qs/-/qs-6.15.1.tgz
Integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==
Package: https://www.npmjs.com/package/qs/v/6.16.0
Archive: https://registry.npmjs.org/qs/-/qs-6.16.0.tgz
Integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==
quick-format-unescaped@4.0.4 | runtime | MIT
Lockfile path: node_modules/quick-format-unescaped
Package: https://www.npmjs.com/package/quick-format-unescaped/v/4.0.4
+7 -6
View File
@@ -1,12 +1,12 @@
{
"name": "rea-agents",
"version": "6.4.0",
"version": "6.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rea-agents",
"version": "6.4.0",
"version": "6.5.0",
"license": "MIT",
"dependencies": {
"@babel/parser": "8.0.4",
@@ -8408,13 +8408,14 @@
"license": "MIT"
},
"node_modules/qs": {
"version": "6.15.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
"integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz",
"integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
"es-define-property": "^1.0.1",
"side-channel": "^1.1.1"
},
"engines": {
"node": ">=0.6"
+4 -1
View File
@@ -1,7 +1,7 @@
{
"name": "rea-agents",
"mcpName": "io.github.morluto/rea",
"version": "6.4.0",
"version": "6.5.0",
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
"license": "MIT",
"repository": {
@@ -353,6 +353,9 @@
"zod": "4.4.3"
},
"overrides": {
"typed-rest-client": {
"qs": "6.16.0"
},
"vitepress": {
"vite": "6.4.4"
}
+2
View File
@@ -10,11 +10,13 @@ const HOST_TEST_OWNERS = ["apple", "package", "sqlite"];
// workflows' release, publication and self-hosted runner safeguards.
const GUARDED_WORKFLOWS = new Set([
".github/workflows/pr-title.yml",
".github/workflows/pr-readiness.yml",
".github/workflows/ci-full-pr.yml",
".github/workflows/release.yml",
".github/workflows/real-ghidra-windows.yml",
".github/workflows/sqlite-runtime.yml",
".github/workflows/ci-timings.yml",
".github/workflows/mutation.yml",
]);
// Sources compiled by build-conformance-fixtures.mjs. The readiness journey
// analyzes the built C fixture as well as its own applications.
+63
View File
@@ -0,0 +1,63 @@
export interface ReadinessPullRequest {
readonly number: number;
readonly state: "open" | "closed";
readonly draft: boolean;
readonly body: string | null;
readonly head: { readonly sha: string };
readonly base: { readonly ref: string };
}
export interface ReadinessResult {
readonly state: "success" | "failure" | "pending";
readonly description: string;
}
export interface ReadinessGitHub {
readonly rest: {
readonly pulls: {
get(input: {
owner: string;
repo: string;
pull_number: number;
}): Promise<{ data: ReadinessPullRequest }>;
};
readonly repos: {
createCommitStatus(
input: ReadinessResult & {
owner: string;
repo: string;
sha: string;
context: string;
target_url: string;
},
): Promise<unknown>;
readonly listPullRequestsAssociatedWithCommit: unknown;
};
};
paginate(
method: unknown,
input: {
owner: string;
repo: string;
commit_sha: string;
per_page: number;
},
): Promise<
readonly Pick<ReadinessPullRequest, "number" | "state" | "head">[]
>;
}
export function evaluateReadiness(
pull: Pick<ReadinessPullRequest, "body" | "draft">,
template: string,
): ReadinessResult;
export function runReadinessCheck(input: {
github: ReadinessGitHub;
context: {
repo: { owner: string; repo: string };
payload: {
pull_request?: { number: number };
inputs?: { pull_request_number?: string };
};
serverUrl: string;
runId: number;
};
core: { info(message: string): void; setFailed(message: string): void };
}): Promise<ReadinessResult | undefined>;
+173
View File
@@ -0,0 +1,173 @@
import { readFile } from "node:fs/promises";
const result = (state, description) => ({ state, description });
function readinessLines(markdown) {
const lines = markdown.split(/\r?\n/u);
const visible = [];
let fence;
let comment = false;
for (const source of lines) {
if (fence) {
if (
new RegExp(
`^ {0,3}${fence.character}{${fence.length},}\\s*$`,
"u",
).test(source)
)
fence = undefined;
continue;
}
const opening = !comment && /^ {0,3}(`{3,}|~{3,})/u.exec(source);
if (opening) {
fence = { character: opening[1][0], length: opening[1].length };
continue;
}
let line = "";
let offset = 0;
while (offset < source.length) {
const marker = comment ? "-->" : "<!--";
const index = source.indexOf(marker, offset);
if (!comment)
line += source.slice(offset, index === -1 ? source.length : index);
if (index === -1) break;
offset = index + marker.length;
comment = !comment;
}
visible.push(line);
}
const starts = visible.flatMap((line, index) =>
/^## Author readiness\s*$/u.test(line) ? [index] : [],
);
if (starts.length !== 1) return [];
const remaining = visible.slice(starts[0] + 1);
const end = remaining.findIndex((line) => /^#{1,2}\s/u.test(line));
return (end === -1 ? remaining : remaining.slice(0, end)).map((line) =>
line.trimEnd(),
);
}
/** Check the initial declaration against the trusted repository template. */
export function evaluateReadiness(pull, template) {
if (pull.draft)
return result(
"pending",
"Draft PR: complete Author readiness before requesting review.",
);
const expected = readinessLines(template).flatMap((line) => {
const match = /^- \[ \] (.+)$/u.exec(line);
return match ? [match[1]] : [];
});
if (expected.length !== 3)
throw new Error(
"Trusted PR template must contain three readiness declarations.",
);
const lines = readinessLines(pull.body ?? "");
const owners = lines.filter((line) => line.startsWith("Human owner:"));
if (
owners.length !== 1 ||
!/^Human owner: @[a-z\d](?:[a-z\d-]*[a-z\d])?$/iu.test(owners[0])
)
return result(
"failure",
"Fill Human owner with one @GitHub-handle in the Author readiness section.",
);
const declarations = lines.flatMap((line) => {
const match = /^- \[([ xX])\] (.+)$/u.exec(line);
return match
? [{ checked: match[1].toLowerCase() === "x", text: match[2] }]
: [];
});
if (
expected.some((text) => {
const matches = declarations.filter((item) => item.text === text);
return matches.length !== 1 || !matches[0].checked;
})
)
return result(
"failure",
"Complete all three Author readiness declarations from the current PR template.",
);
return result(
"success",
"Human owner and all three initial readiness declarations are filled.",
);
}
/** Publish readiness on the PR head without running contributor code. */
export async function runReadinessCheck({ github, context, core }) {
const number =
context.payload.pull_request?.number ??
Number(context.payload.inputs?.pull_request_number);
if (!Number.isSafeInteger(number) || number <= 0)
throw new Error("A positive pull request number is required.");
const repository = context.repo;
const getPull = async () =>
(await github.rest.pulls.get({ ...repository, pull_number: number })).data;
const pull = await getPull();
if (pull.state !== "open" || !/^(?:main|release\/.+)$/u.test(pull.base.ref))
return;
const publish = (sha, verdict) =>
github.rest.repos.createCommitStatus({
...repository,
sha,
context: "PR readiness",
...verdict,
target_url: `${context.serverUrl}/${repository.owner}/${repository.repo}/actions/runs/${context.runId}`,
});
await publish(
pull.head.sha,
result("pending", "Checking the initial author readiness declaration."),
);
const template = await readFile(
new URL("../../.github/pull_request_template.md", import.meta.url),
"utf8",
);
let verdict = evaluateReadiness(pull, template);
if (verdict.state === "success") {
const associated = await github.paginate(
github.rest.repos.listPullRequestsAssociatedWithCommit,
{ ...repository, commit_sha: pull.head.sha, per_page: 100 },
);
if (
!associated.some(
(other) =>
other.number === number &&
other.state === "open" &&
other.head.sha === pull.head.sha,
)
)
verdict = result(
"pending",
"GitHub has not confirmed this PR's head association; rerun PR readiness.",
);
else if (
associated.some(
(other) =>
other.state === "open" &&
other.number !== number &&
other.head.sha === pull.head.sha,
)
)
verdict = result(
"failure",
"Open PRs must have distinct head commits because GitHub shares commit statuses.",
);
}
const latest = await getPull();
if (latest.state !== "open") return;
if (
latest.head.sha !== pull.head.sha ||
latest.body !== pull.body ||
latest.draft !== pull.draft ||
latest.base.ref !== pull.base.ref
)
verdict = result(
"pending",
"PR changed during the check; wait for the latest run or rerun PR readiness.",
);
await publish(latest.head.sha, verdict);
core.info(verdict.description);
if (verdict.state === "failure") core.setFailed(verdict.description);
return verdict;
}
+2
View File
@@ -3,6 +3,7 @@
// every workflow.
export const SCOPES = {
reverse_tools: [
".github/workflows/mutation.yml",
"src/application/reverse/**",
"src/application/binary/BinarySession.ts",
"src/cutter/**",
@@ -23,6 +24,7 @@ export const SCOPES = {
"tests/composition/analysis-sessions/binarySession.test.ts",
"tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts",
"tests/boundary/filesystem/reverseToolsMutationConfig.test.ts",
"tests/boundary/filesystem/mutationWorkflow.test.ts",
"src/server/registerRizinDebugTools.ts",
"tests/boundary/providers/CutterBridge*.test.ts",
"tests/boundary/providers/cutterBridgeProducer.test.ts",
@@ -12,6 +12,20 @@ import { browserScenarioSchema } from "../../dist/domain/browserScenario.js";
import { runScenarioCli } from "./browser-scenario-verifier.mjs";
import { verifyScenarioPolicySetup } from "./browser-scenario-service-worker-setup-e2e.mjs";
const diagnosticError = (error) =>
error instanceof Error
? {
name: error.name,
message: error.message.slice(0, 1_000),
reason: error.reason,
stack: error.stack?.slice(0, 2_000),
cause:
error.cause instanceof Error
? error.cause.stack?.slice(0, 3_000)
: typeof error.cause,
}
: { kind: typeof error };
const startSite = async () => {
let otherOrigin;
const server = createServer((request, response) => {
@@ -83,6 +97,7 @@ export async function verifyScenarioServiceWorkers(endpoint, targetId) {
first.setOtherOrigin(second.origin);
second.setOtherOrigin(first.origin);
const observer = await chromium.connectOverCDP(endpoint);
console.log("Service-worker fixture browser:", observer.version());
let provider = createBrowserScenarioProvider(process.env);
let session;
let failure;
@@ -154,7 +169,42 @@ export async function verifyScenarioServiceWorkers(endpoint, targetId) {
state: "visible",
},
]);
const cleanCapture = (await runScenarioCli(clean)).normalized_result;
let cleanCapture;
try {
cleanCapture = (await runScenarioCli(clean)).normalized_result;
} catch (cause) {
// Retain the original failure. One bounded replay navigates only this
// verifier-owned target and exposes causes the CLI does not serialize.
const state = await page
.locator("#state")
.evaluate(
(element) => ({
text: element.textContent?.slice(0, 100),
blocked: element.getAttribute("data-blocked"),
nativeRegister: navigator.serviceWorker.register
.toString()
.includes("[native code]"),
}),
undefined,
{ timeout: 1_000 },
)
.catch((error) => ({ unavailable: String(error).slice(0, 500) }));
console.error("Service-worker CLI failure state:", {
url: page.url(),
state,
});
const replay = await provider
.captureScenario(clean, { signal: AbortSignal.timeout(5_000) })
.then((result) =>
result.ok ? { ok: true } : diagnosticError(result.error),
)
.catch(diagnosticError);
console.error(
"Service-worker diagnostic replay (original CLI failure retained):",
replay,
);
throw cause;
}
assert.ok(cleanCapture.steps.every(({ status }) => status === "completed"));
assert.equal(cleanCapture.browser.cleanup, "disconnected-external");
await assertRestored();
+2 -2
View File
@@ -98,9 +98,9 @@ export async function readReleaseHistory(run, base, target) {
return { base: baseSha, target: targetSha, commits };
}
// REA history maps Anthropic and Cursor's agent trailers to ordinary User actors.
// GitHub maps these coding-agent trailers to ordinary User actors.
// Keep those source identities in the inventory while excluding automation credit.
const AGENT_ACCOUNTS = new Set(["claude", "cursoragent"]);
const AGENT_ACCOUNTS = new Set(["claude", "cursoragent", "codex"]);
// REA's maintainer requested external-contributor thanks without self-credit.
const MAINTAINER_ACCOUNTS = new Set(["morluto"]);
const human = (value) =>
+12 -3
View File
@@ -314,12 +314,21 @@ class WebsiteTranslationChecks(unittest.TestCase):
def test_broken_placeholders_and_inline_markup_fail_before_replacing_preview(self):
with self.translated_project() as root:
page = root / "website/public/index.html"
page.write_text(page.read_text().replace(
"</main>",
'<p>Translation test <a href="get-started/">read more</a>.</p></main>',
))
extract_catalogs(root)
path = root / "website/locales/zh-CN/messages.po"
catalog = polib.pofile(path)
entry = catalog.find("Translation test {ph0}read more{ph1}.", msgctxt="page:index.html")
self.assertIsNotNone(entry)
entry.msgstr = "翻译测试:{ph0}继续阅读{ph1}。"
catalog.save(path)
site = root / "website/_site"
build_translations(root, site)
before = (site / "zh-CN/index.html").read_bytes()
path = root / "website/locales/zh-CN/messages.po"
catalog = polib.pofile(path)
entry = catalog.find("Already know RE? {ph0}Skip to analysis guides →{ph1}", msgctxt="page:index.html")
original = entry.msgstr
for bad, reason in (("丢失链接", "changed placeholders"), ("{ph1}错误的链接{ph0}", "invalid inline markup")):
with self.subTest(reason=reason):
+2 -2
View File
@@ -8,13 +8,13 @@
"url": "https://github.com/morluto/rea",
"source": "github"
},
"version": "6.4.0",
"version": "6.5.0",
"packages": [
{
"registryType": "npm",
"registryBaseUrl": "https://registry.npmjs.org",
"identifier": "rea-agents",
"version": "6.4.0",
"version": "6.5.0",
"runtimeHint": "npx",
"transport": {
"type": "stdio"
+8
View File
@@ -55,4 +55,12 @@ describe("parseAdbVersionOutput", () => {
installedPath: null,
});
});
it("does not accept arbitrary successful output as an adb version", () => {
expect(
parseAdbVersionOutput(
"Android Debug Bridge version tomorrow\nVersion unknown\n",
),
).toMatchObject({ version: null });
});
});
+30 -8
View File
@@ -67,7 +67,9 @@ export interface AdbClientIdentity {
* `Version ` the build version (no colon), and `Installed as` the binary path.
*/
export const parseAdbVersionOutput = (stdout: string): AdbClientIdentity => {
const build = /^Version\s+(\S.*)$/mu.exec(stdout);
const build = /^Version\s+(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)\s*$/mu.exec(
stdout,
);
const installed = /^Installed as\s+(.+)$/mu.exec(stdout);
return {
version: build === null ? null : build[1]!.trim(),
@@ -84,14 +86,34 @@ export const inspectAdbClient = async (
signal?: AbortSignal,
): Promise<AdbClientIdentity> => {
try {
const { stdout } = await execFileOutput(selection.binary, ["version"], {
timeout: VERSION_TIMEOUT_MS,
stopSignal: "SIGTERM",
maxBuffer: VERSION_MAX_OUTPUT_BYTES,
...(signal === undefined ? {} : { signal }),
});
return parseAdbVersionOutput(stdout);
const { stdout, stderr } = await execFileOutput(
selection.binary,
["version"],
{
timeout: VERSION_TIMEOUT_MS,
stopSignal: "SIGTERM",
maxBuffer: VERSION_MAX_OUTPUT_BYTES,
...(signal === undefined ? {} : { signal }),
},
);
const identity = parseAdbVersionOutput(stdout);
const hasRecognizedProtocolVersion =
/^Android Debug Bridge version\s+\d+\.\d+\.\d+\s*$/mu.test(stdout);
if (identity.version === null && !hasRecognizedProtocolVersion)
throw new AdbConfigurationFailure(
"version_unresolved",
"The selected adb binary exited successfully but did not report a recognizable protocol or build version",
{
selected_binary: selection.binary,
path_source: selection.pathSource,
version_probe: "unrecognized_output",
stdout,
stderr,
},
);
return identity;
} catch (cause) {
if (cause instanceof AdbConfigurationFailure) throw cause;
const failure = execFileOutputFailure(cause);
if (signal?.aborted || failure?.code === "ABORT_ERR")
throw new AnalysisCancelledError("inspect_adb_client", { cause });
+63
View File
@@ -56,7 +56,9 @@ describe("parseAdbDevicesOutput", () => {
},
]);
});
});
describe("ADB daemon startup notices", () => {
it("recognizes daemon startup notices instead of failing the listing", () => {
const output = [
"* daemon not running; starting now at tcp:5037",
@@ -70,6 +72,67 @@ describe("parseAdbDevicesOutput", () => {
expect(devices).toHaveLength(1);
});
it("recognizes daemon startup notices emitted on stderr", () => {
const stdout = [
"List of devices attached",
"emulator-5554 device transport_id:1",
"",
].join("\n");
const stderr = [
"* daemon not running; starting now at tcp:5037",
"* daemon started successfully",
"",
].join("\n");
const { devices, daemonStarted, unparsedLines } = parseAdbDevicesOutput(
stdout,
stderr,
);
expect(daemonStarted).toBe(true);
expect(unparsedLines).toEqual([]);
expect(devices).toHaveLength(1);
});
it("recognizes the legacy daemon notice with its trailing marker and CRLF", () => {
const { daemonStarted, unparsedLines } = parseAdbDevicesOutput(
"List of devices attached\r\n",
"* daemon started successfully *\r\n",
);
expect(daemonStarted).toBe(true);
expect(unparsedLines).toEqual([]);
});
it("does not treat a daemon startup error containing the success phrase as success", () => {
const { daemonStarted, unparsedLines } = parseAdbDevicesOutput(
"List of devices attached\r\n",
"* daemon not started successfully\r\n",
);
expect(daemonStarted).toBe(false);
expect(unparsedLines).toEqual([]);
});
it("ignores non-startup lines and device strings on stderr", () => {
const stdout = [
"List of devices attached",
"emulator-5554 device transport_id:1",
"",
].join("\n");
const stderr = [
"adb: warning: some transport warning",
"emulator-5556 device transport_id:2",
"",
].join("\n");
const { devices, daemonStarted, unparsedLines } = parseAdbDevicesOutput(
stdout,
stderr,
);
expect(daemonStarted).toBe(false);
expect(unparsedLines).toEqual([]);
expect(devices).toHaveLength(1);
expect(devices[0]?.serial).toBe("emulator-5554");
});
});
describe("ADB device states", () => {
it("keeps non-device states such as unauthorized and offline", () => {
const output = [
"List of devices attached",
+21 -4
View File
@@ -30,6 +30,12 @@ export interface AdbDeviceEntry {
const DEVICE_LINE = /^(\S+)\s+(.*)$/u;
const normalizeAdbOutputLine = (rawLine: string): string => rawLine.trimEnd();
const isSuccessfulDaemonStartNotice = (line: string): boolean =>
line === "* daemon started successfully" ||
line === "* daemon started successfully *";
const metadataValue = (remainder: string, key: string): string | null => {
const match = new RegExp(`(?:^|\\s)${key}:(\\S+)`, "u").exec(remainder);
return match === null ? null : match[1]!;
@@ -42,6 +48,7 @@ const metadataValue = (remainder: string, key: string): string | null => {
*/
export const parseAdbDevicesOutput = (
stdout: string,
stderr = "",
): {
devices: readonly AdbDeviceEntry[];
unparsedLines: readonly string[];
@@ -51,12 +58,13 @@ export const parseAdbDevicesOutput = (
const unparsedLines: string[] = [];
let daemonStarted = false;
for (const rawLine of stdout.split(/\r?\n/u)) {
const line = rawLine.trimEnd();
if (line === "" || line.startsWith("List of devices attached")) continue;
if (line.startsWith("* daemon")) {
if (line.includes("started successfully")) daemonStarted = true;
const line = normalizeAdbOutputLine(rawLine);
if (isSuccessfulDaemonStartNotice(line)) {
daemonStarted = true;
continue;
}
if (line === "" || line.startsWith("List of devices attached")) continue;
if (line.startsWith("* daemon")) continue;
const match = DEVICE_LINE.exec(line);
const state = ADB_DEVICE_STATES.find((candidate) => {
const remainder = match?.[2];
@@ -105,6 +113,15 @@ export const parseAdbDevicesOutput = (
: "none",
});
}
if (!daemonStarted && stderr !== "") {
for (const rawLine of stderr.split(/\r?\n/u)) {
const line = normalizeAdbOutputLine(rawLine);
if (isSuccessfulDaemonStartNotice(line)) {
daemonStarted = true;
break;
}
}
}
return { devices, unparsedLines, daemonStarted };
};
+69 -3
View File
@@ -91,6 +91,36 @@ it("inspects the adb client without contacting the server", async () => {
expect(result.contacted_adb_server).toBe(false);
});
it("fails closed when adb exits successfully without a recognized version", async () => {
const stdout = `unexpected adb output ${"x".repeat(700)}\n`;
const stderr = `probe stderr ${"y".repeat(700)}\n`;
const { binary } = await writeAdbStub(
() => `printf '%s' '${stdout}'; printf '%s' '${stderr}' >&2; exit 0`,
);
await expect(
providerOver(binary).inspectAvailability(),
).resolves.toMatchObject({
status: "unavailable",
code: "version_unresolved",
diagnostics: {
version_probe: "unrecognized_output",
stdout,
stderr,
},
});
});
it("accepts adb's recognized protocol version when the build version is absent", async () => {
const { binary } = await writeAdbStub(
() => `printf 'Android Debug Bridge version 1.0.41\\n'`,
);
await expect(
providerOver(binary).inspectAvailability(),
).resolves.toMatchObject({
status: "available",
});
});
it("lists devices with inferred kinds and observed daemon startup", async () => {
const { binary } = await writeAdbStub(
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
@@ -121,6 +151,36 @@ fi`,
]);
});
it("observes daemon startup when notices are printed to stderr", async () => {
const { binary } = await writeAdbStub(
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
if [ "$1" = "devices" ]; then
printf '* daemon not running; starting now at tcp:5037\\n* daemon started successfully\\n' >&2
printf 'List of devices attached\\n'
printf 'emulator-5554 device product:sdk_gphone64_x86_64 model:sdk_gphone64_x86_64 device:emu64xa transport_id:1\\n'
exit 0
fi`,
);
const result = await evidenceResult(providerOver(binary), {
operation: "list_adb_devices",
input: {},
});
expect(result.may_have_started_adb_server).toBe(true);
expect(result.devices).toEqual([
{
serial: "emulator-5554",
state: "device",
transport: "tcp",
product: "sdk_gphone64_x86_64",
model: "sdk_gphone64_x86_64",
device: "emu64xa",
transport_id: 1,
kind: "emulator",
kind_basis: "emulator_serial_prefix",
},
]);
});
it("refuses device listings with an invalid state token", async () => {
const malformedLine = "serial deviceXYZ ";
const { binary } = await writeAdbStub(
@@ -1162,15 +1222,21 @@ exit 1`,
const apk = join(root, "app.apk");
await writeFile(apk, "apk");
const digest = createHash("sha256").update("apk").digest("hex");
const installed = await evidenceResult(providerOver(binary), {
const execution = await providerOver(binary).execute({
operation: "install_adb_package",
input: { serial: "emulator-5554", apk_path: apk, replace: false },
});
expect(installed).toMatchObject({
expect(execution.ok).toBe(true);
if (!execution.ok) throw execution.error;
expect(execution.value.result).toMatchObject({
bytes: 3,
sha256: digest,
replaced: false,
replace_requested: false,
replaced: null,
});
expect(execution.value.limitations).toContain(
"Whether an existing package was replaced is unknown; replace_requested records only the adb install mode",
);
const { binary: failing } = await writeAdbStub(
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
+4 -1
View File
@@ -755,6 +755,7 @@ export class AdbProvider {
if (!ran.ok) return ran;
const { devices, unparsedLines, daemonStarted } = parseAdbDevicesOutput(
ran.value.stdout,
ran.value.stderr,
);
if (unparsedLines.length > 0)
return err(
@@ -1883,12 +1884,14 @@ export class AdbProvider {
apk_path: apkPath,
bytes: digest.bytes,
sha256: digest.sha256,
replaced: input.replace,
replace_requested: input.replace,
replaced: null,
},
provider,
{
limitations: [
"Install changes device state; the installed APK's digest is the host file's digest",
"Whether an existing package was replaced is unknown; replace_requested records only the adb install mode",
],
},
),
+22 -1
View File
@@ -58,7 +58,28 @@ describe("parseApktoolVersionOutput", () => {
});
it("reports null when the launcher prints nothing usable", () => {
expect(parseApktoolVersionOutput(" \n")).toEqual({ version: null });
expect(parseApktoolVersionOutput(" \n")).toEqual({
version: null,
reportedVersion: null,
});
expect(parseApktoolVersionOutput("successful but not a version\n")).toEqual(
{
version: null,
reportedVersion: "successful but not a version",
},
);
});
it("parses semantic prerelease versions without claiming they meet the minimum", () => {
expect(parseApktoolVersionOutput("2.7.0-beta.1\n")).toEqual({
version: "2.7.0-beta.1",
});
});
it("validates normalized semantic versions while preserving caller spelling", () => {
expect(parseApktoolVersionOutput("v2.7.0\n")).toEqual({
version: "v2.7.0",
});
});
});
+52 -10
View File
@@ -15,6 +15,7 @@ import {
import type { ProviderAvailability } from "../application/AnalysisProvider.js";
import type { ProviderRejectionCode } from "../contracts/providerSelection.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { gte, valid } from "semver";
import {
isWindowsBatchFile,
windowsBatchCommand,
@@ -148,7 +149,8 @@ export type ApktoolConfigurationFailureCode =
| "command_missing"
| "command_unresolved"
| "launcher_unsupported"
| "version_unresolved";
| "version_unresolved"
| "unsupported_version";
export class ApktoolConfigurationFailure extends Error {
constructor(
@@ -161,9 +163,9 @@ export class ApktoolConfigurationFailure extends Error {
}
}
export interface ApktoolClientIdentity {
readonly version: string | null;
}
export type ApktoolClientIdentity =
| { readonly version: string; readonly reportedVersion?: never }
| { readonly version: null; readonly reportedVersion: string | null };
/** Process creation for one apktool run on the selected host. */
export interface ApktoolInvocation {
@@ -199,7 +201,7 @@ export const apktoolInvocation = (
windowsVerbatimArguments: false,
};
/** Read the version from `apktool --version` output (first non-empty line). */
/** Read the version from Apktool's single-line `--version` response. */
export const parseApktoolVersionOutput = (
stdout: string,
): ApktoolClientIdentity => {
@@ -207,7 +209,17 @@ export const parseApktoolVersionOutput = (
.split(/\r?\n/u)
.map((entry) => entry.trim())
.find((entry) => entry !== "");
return { version: line === undefined ? null : line };
if (line === undefined || valid(line) === null)
return { version: null, reportedVersion: line ?? null };
return { version: line };
};
const supportsDocumentedApktoolVersion = (version: string): boolean => {
// Debian's verified 2.7.0-dirty build is a downstream build marker, not a
// prerelease. Treat only that known marker as build metadata for the floor.
const comparisonVersion = version.replace(/-dirty$/u, "+dirty");
const parsedVersion = valid(comparisonVersion);
return parsedVersion !== null && gte(parsedVersion, "2.7.0");
};
/** Probe the selected apktool launcher without decoding anything. */
@@ -239,8 +251,10 @@ export const inspectApktoolClient = async (
{ cause },
);
}
let stdout: string;
let stderr: string;
try {
const { stdout } = await execFileOutput(
({ stdout, stderr } = await execFileOutput(
invocation.command,
invocation.arguments,
{
@@ -251,8 +265,7 @@ export const inspectApktoolClient = async (
env: hostEnvironment,
...(signal === undefined ? {} : { signal }),
},
);
return parseApktoolVersionOutput(stdout);
));
} catch (cause) {
const failure = execFileOutputFailure(cause);
if (signal?.aborted || failure?.code === "ABORT_ERR")
@@ -283,6 +296,33 @@ export const inspectApktoolClient = async (
{ cause },
);
}
const identity = parseApktoolVersionOutput(stdout);
if (identity.version === null)
throw new ApktoolConfigurationFailure(
"version_unresolved",
"The selected apktool launcher exited successfully but did not report a recognizable version",
{
selected_command: selection.command,
command_source: selection.commandSource,
version_probe: "unrecognized_output",
reported_version: identity.reportedVersion,
stdout,
stderr,
},
);
if (!supportsDocumentedApktoolVersion(identity.version))
throw new ApktoolConfigurationFailure(
"unsupported_version",
`The selected Apktool version ${identity.version} is older than the supported minimum 2.7.0`,
{
selected_command: selection.command,
command_source: selection.commandSource,
reported_version: identity.version,
minimum_version: "2.7.0",
remediation: `Use Apktool 2.7.0 or newer; see docs/apktool-resource-analysis.md.`,
},
);
return identity;
};
/** Availability in the shape shared by every provider. */
@@ -295,7 +335,9 @@ const rejectionCodeOf = (
code === "launcher_unsupported" ||
code === "command_unresolved"
? "not_configured"
: "version_unresolved";
: code === "unsupported_version"
? "unsupported_version"
: "version_unresolved";
export const apktoolConfigurationAvailability = (
cause: ApktoolConfigurationFailure,
+35 -2
View File
@@ -17,7 +17,7 @@ import type { ApktoolRequest } from "../domain/apktool/apktoolResourceAnalysis.j
const it = test.skipIf(process.platform === "win32");
const VERSION_BLOCK = "printf '2.7.0-stub\\n'";
const VERSION_BLOCK = "printf '2.7.0-dirty\\n'";
const REAL_YML = [
"!!brut.androlib.meta.MetaInfo",
@@ -172,7 +172,7 @@ it("inspects the apktool launcher", async () => {
expect(result.client).toEqual({
command,
command_source: "environment",
apktool_version: "2.7.0-stub",
apktool_version: "2.7.0-dirty",
});
});
@@ -355,6 +355,39 @@ it("reports a missing launcher as a capability gap with recovery", async () => {
});
});
it("fails closed for unrecognized and unsupported Apktool versions", async () => {
const invalidOutput = `unexpected Apktool output ${"x".repeat(700)}\n`;
const unrecognized = await writeApktoolStub(
() => `printf '%s' '${invalidOutput}'; exit 0`,
);
await expect(
providerOver(unrecognized.command).inspectAvailability(),
).resolves.toMatchObject({
status: "unavailable",
code: "version_unresolved",
diagnostics: {
reported_version: invalidOutput.trim(),
stdout: invalidOutput,
},
});
const old = await writeApktoolStub(() => `printf '2.6.3\\n'`);
await expect(
providerOver(old.command).inspectAvailability(),
).resolves.toMatchObject({
status: "unavailable",
code: "unsupported_version",
});
const prerelease = await writeApktoolStub(() => `printf '2.7.0-beta.1\\n'`);
await expect(
providerOver(prerelease.command).inspectAvailability(),
).resolves.toMatchObject({
status: "unavailable",
code: "unsupported_version",
});
});
it("rejects a target that is not a readable file", async () => {
const { command, root } = await writeApktoolStub(decodeStub());
const projected = await refusedBy(providerOver(command), {
+4
View File
@@ -223,6 +223,10 @@ export const parseBinaryTarget = async (
}
} catch (cause: unknown) {
if (cause instanceof AnalysisCancelledError) return err(cause);
if (signal?.aborted === true && cause === signal.reason)
return err(
new AnalysisCancelledError("resolve_binary_target", { cause }),
);
if (cause instanceof BinaryTargetError) return err(cause);
if (!(cause instanceof Error)) throw cause;
const failure = classifyBinaryTargetFilesystemFailure(candidate, cause);
@@ -97,6 +97,53 @@ const entry = (
return found;
};
describe("ADB and Apktool host availability", () => {
it("uses only their selected owner probes", () => {
const ready = {
status: "available" as const,
code: null,
reason: null,
diagnostics: {},
};
const configuredButInvalid = {
status: "unavailable" as const,
code: "not_configured" as const,
reason: "REA_ADB_PATH is invalid",
diagnostics: {},
};
const readyInventory = buildCapabilityInventory(status(), {
...enabledPolicy,
adbAnalysisAvailability: ready,
apktoolAnalysisAvailability: ready,
});
expect(
readyInventory.find(({ name }) => name === "inspect_adb_client"),
).toMatchObject({ available: true, reason: "available" });
expect(
readyInventory.find(({ name }) => name === "inspect_apktool_client"),
).toMatchObject({ available: true, reason: "available" });
const unavailableInventory = buildCapabilityInventory(status(), {
...enabledPolicy,
adbAnalysisAvailability: configuredButInvalid,
apktoolAnalysisAvailability: {
status: "unavailable",
code: "unsupported_version",
reason: "Apktool 2.6.3 is older than 2.7.0",
diagnostics: {},
},
});
expect(
unavailableInventory.find(({ name }) => name === "inspect_adb_client"),
).toMatchObject({ available: false, reason: "provider_missing" });
expect(
unavailableInventory.find(
({ name }) => name === "decode_android_resources",
),
).toMatchObject({ available: false, reason: "provider_unavailable" });
});
});
describe("capability failure reasons", () => {
it("keeps an unavailable Android runtime distinct from a missing provider", () => {
const inventory = buildCapabilityInventory(status(), {
+31
View File
@@ -54,6 +54,8 @@ export type AvailabilityPolicy = {
readonly firmwareInspectionEnabled?: boolean;
readonly firmwareExtractionEnabled?: boolean;
readonly androidAnalysisAvailability?: ProviderAvailability;
readonly adbAnalysisAvailability?: ProviderAvailability;
readonly apktoolAnalysisAvailability?: ProviderAvailability;
readonly jebAnalysisAvailability?: ProviderAvailability;
readonly javascriptRecoveryEnabled?: boolean;
readonly webModuleResolutionEnabled?: boolean;
@@ -473,6 +475,16 @@ const workflowAvailabilityFor = ({
policy.androidAnalysisAvailability?.reason ??
"Set REA_JADX_MCP_JAR to a caller-supplied jadx-headless-mcp 0.7.1 JAR and provide a full JDK on Linux, macOS, or Windows x64 with its matching bundled native controls.",
};
if (kind === "adb-provider")
return ownerProbeAvailability(
policy.adbAnalysisAvailability,
"Set REA_ADB_PATH to a caller-supplied adb executable or make adb available on PATH. This checks the client version only; device and serial availability depend on the request.",
);
if (kind === "apktool-provider")
return ownerProbeAvailability(
policy.apktoolAnalysisAvailability,
"Set REA_APKTOOL_COMMAND to a caller-supplied Apktool executable or make Apktool 2.7.0 or newer available on PATH.",
);
if (kind === "jeb-provider")
return policy.jebAnalysisAvailability?.status === "available"
? { reason: "available", remediation: null }
@@ -506,6 +518,25 @@ const workflowAvailabilityFor = ({
return null;
};
const ownerProbeAvailability = (
observation: ProviderAvailability | undefined,
missingRemediation: string,
): Availability => {
if (observation === undefined)
return { reason: "provider_missing", remediation: missingRemediation };
if (observation.status === "available")
return { reason: "available", remediation: null };
return {
reason:
observation.code === "unsupported_host"
? "unsupported_host"
: observation.code === "not_configured"
? "provider_missing"
: "provider_unavailable",
remediation: observation.reason,
};
};
const electronProviderAvailability = (
name: string,
kind: ToolKind,
+48 -4
View File
@@ -1,4 +1,4 @@
import { basename, isAbsolute, resolve } from "node:path";
import { isAbsolute, resolve } from "node:path";
import { valid } from "semver";
import { PRODUCT_IDENTITY } from "../identity.js";
@@ -112,13 +112,56 @@ const isOwnedPackageReference = (packageReference: string): boolean => {
};
/** Check that one parsed command points to REA's MCP entry point. */
export type ClientRegistrationCommandIdentity = "owned" | "foreign" | "unknown";
/** Classify only what the command and current executable identity establish. */
export const classifyClientRegistrationCommand = (
command: readonly string[],
currentCommandPath: string | undefined,
): ClientRegistrationCommandIdentity => {
const packageReference = clientRegistrationPackageReference(command);
if (packageReference !== undefined)
return isOwnedClientRegistrationCommand(command, currentCommandPath)
? "owned"
: "foreign";
const currentPathAvailable =
currentCommandPath !== undefined && currentCommandPath.length > 0;
const nodeScript = command.length === 3 && command[2] === "mcp";
if (nodeScript) {
if (
!isAbsolute(command[0] ?? "") ||
!isAbsolute(command[1] ?? "") ||
!currentPathAvailable
)
return "unknown";
return isOwnedClientRegistrationCommand(command, currentCommandPath)
? "owned"
: "foreign";
}
if (command.length === 2 && command[1] === "mcp") {
const executable = command[0] ?? "";
if (executable === PRODUCT_IDENTITY.cliBinary) return "owned";
const relativePathLike =
!isAbsolute(executable) && /[./\\]/u.test(executable);
if (relativePathLike || (isAbsolute(executable) && !currentPathAvailable))
return "unknown";
}
if (isOwnedClientRegistrationCommand(command, currentCommandPath))
return "owned";
return "foreign";
};
export const isOwnedClientRegistrationCommand = (
command: readonly string[],
currentCommandPath: string = resolve(process.argv[1] ?? "unknown"),
currentCommandPath: string | undefined,
): boolean => {
if (
currentCommandPath !== undefined &&
currentCommandPath.length > 0 &&
command.length === 3 &&
command[2] === "mcp" &&
isAbsolute(command[0] ?? "") &&
isAbsolute(command[1] ?? "") &&
resolve(command[0] ?? "") === resolve(process.execPath) &&
resolve(command[1] ?? "") === currentCommandPath
)
@@ -129,8 +172,9 @@ export const isOwnedClientRegistrationCommand = (
if (
executable === PRODUCT_IDENTITY.cliBinary ||
(isAbsolute(executable) &&
basename(executable) === PRODUCT_IDENTITY.cliBinary) ||
resolve(executable) === currentCommandPath
currentCommandPath !== undefined &&
currentCommandPath.length > 0 &&
resolve(executable) === currentCommandPath)
)
return true;
}
+71 -42
View File
@@ -7,16 +7,17 @@ import {
type ClientConfigurationDocument,
} from "./ClientConfigurationDocument.js";
import { access } from "node:fs/promises";
import { resolve } from "node:path";
import { isAbsolute, resolve } from "node:path";
import { z } from "zod";
import { valid } from "semver";
import { PRODUCT_IDENTITY } from "../identity.js";
import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js";
import { MCP_STARTUP_POLICY } from "../mcpStartupPolicy.js";
import {
clientRegistrationPackageReference,
isOwnedClientRegistrationCommand,
classifyClientRegistrationCommand,
packageRunnerRegistrationCommand,
} from "./ClientRegistrationIdentity.js";
import {
@@ -50,6 +51,11 @@ export type ClientRegistrationStatus = ClientRegistrationStatusBase &
readonly state: "stale";
readonly remediation: string;
}
| {
readonly command: RegistrationCommand;
readonly state: "unknown";
readonly remediation: string;
}
| {
readonly command: readonly [];
readonly state: "missing" | "invalid" | "manual";
@@ -93,7 +99,7 @@ export interface ClientRegistrationInspection {
/** Project diagnostics from the same inspection used to select maintenance. */
export const readClientRegistrationStatuses = async (
home: string,
currentCommandPath: string = resolve(process.argv[1] ?? "unknown"),
currentCommandPath: string | undefined = CURRENT_ENTRYPOINT_PATH,
options: ClientRegistrationReadOptions = {},
): Promise<readonly ClientRegistrationStatus[]> =>
(
@@ -103,7 +109,7 @@ export const readClientRegistrationStatuses = async (
/** Parse effective registrations and their client-specific activation once. */
export const readClientRegistrationInspections = async (
home: string,
currentCommandPath: string = resolve(process.argv[1] ?? "unknown"),
currentCommandPath: string | undefined = CURRENT_ENTRYPOINT_PATH,
options: ClientRegistrationReadOptions = {},
): Promise<readonly ClientRegistrationInspection[]> => {
const inspections: ClientRegistrationInspection[] = [];
@@ -219,26 +225,32 @@ export const readClientRegistrationInspections = async (
const packageVersionDrift =
packageVersion !== undefined &&
packageVersion !== PRODUCT_IDENTITY.packageVersion;
const alignment = registrationAligned(
registration,
effectiveClient,
currentCommandPath,
options.platform ?? process.platform,
);
record(
configuredStatus(
effectiveClient,
command,
activation === "enabled" &&
!packageVersionDrift &&
registrationAligned(
registration,
effectiveClient,
currentCommandPath,
options.platform ?? process.platform,
) &&
(parsed.dialect !== "dsh" || dshDocument(parsed.document).ready) &&
policyBlock === undefined
? "aligned"
: "stale",
alignment === "unknown"
? "unknown"
: activation === "enabled" &&
!packageVersionDrift &&
alignment === "aligned" &&
(parsed.dialect !== "dsh" ||
dshDocument(parsed.document).ready) &&
policyBlock === undefined
? "aligned"
: "stale",
policyBlock ??
(packageVersionDrift
? packageVersionRemediation(packageReference ?? "", client)
: undefined),
: alignment === "unknown"
? unknownEntryPointRemediation()
: undefined),
packageReference,
),
activation,
@@ -275,25 +287,29 @@ const registrationActivation = (
const registrationAligned = (
registration: z.output<typeof registrationSchema>,
client: SetupClient,
currentCommandPath: string,
currentCommandPath: string | undefined,
platform: NodeJS.Platform,
): boolean => {
): "aligned" | "stale" | "unknown" => {
const command = [registration.command, ...registration.args];
if (!isOwnedClientRegistrationCommand(command, currentCommandPath))
return false;
const identity = classifyClientRegistrationCommand(
command,
currentCommandPath,
);
if (identity === "unknown") return "unknown";
if (identity === "foreign") return "stale";
if (
(client.name === "codex" || client.name === "grok_build") &&
registration.startup_timeout_sec !==
MCP_STARTUP_POLICY.codexStartupTimeoutSeconds
)
return false;
return "stale";
if (
client.name === "kimi" &&
(registration.startupTimeoutMs !==
MCP_STARTUP_POLICY.kimiStartupTimeoutMs ||
registration.toolTimeoutMs !== MCP_STARTUP_POLICY.kimiToolTimeoutMs)
)
return false;
return "stale";
if (
(client.format === "kimi" ||
client.format === "omp" ||
@@ -301,45 +317,55 @@ const registrationAligned = (
registration.type !== undefined &&
registration.type !== "stdio"
)
return false;
if (client.format === "vscode" && registration.type !== "stdio") return false;
return "stale";
if (client.format === "vscode" && registration.type !== "stdio")
return "stale";
// A Harness registration only connects as the rea stdio server; another
// server name or transport leaves the session without REA tools.
if (
client.format === "dsh" &&
(registration.serverName !== "rea" || registration.transport !== "stdio")
)
return false;
return "stale";
if (
client.format === "copilot_cli" &&
(registration.type !== "stdio" ||
JSON.stringify(registration.tools) !== JSON.stringify(["*"]))
)
return false;
return "stale";
if (client.format === "commandcode" && registration.transport !== "stdio")
return false;
if (
command.length === 3 &&
command[2] === "mcp" &&
resolve(command[0] ?? "") === resolve(process.execPath) &&
resolve(command[1] ?? "") === currentCommandPath
)
return true;
return "stale";
if (
(["npx", "bunx"] as const).some((runner) => {
const runnerCommand = packageRunnerRegistrationCommand(runner, platform);
const expected = packageRunnerRegistrationCommand(runner, platform);
return (
command.length === runnerCommand.length &&
command.every((argument, index) => argument === runnerCommand[index])
command.length === expected.length &&
command.every((argument, index) => argument === expected[index])
);
})
)
return true;
return (
return "aligned";
if (
currentCommandPath !== undefined &&
currentCommandPath.length > 0 &&
command.length === 3 &&
command[2] === "mcp" &&
isAbsolute(command[0] ?? "") &&
isAbsolute(command[1] ?? "") &&
resolve(command[0] ?? "") === resolve(process.execPath) &&
resolve(command[1] ?? "") === currentCommandPath
)
return "aligned";
if (
currentCommandPath !== undefined &&
currentCommandPath.length > 0 &&
command.length === 2 &&
command[1] === "mcp" &&
isAbsolute(command[0] ?? "") &&
resolve(command[0] ?? "") === currentCommandPath
);
)
return "aligned";
return "stale";
};
const parseRegistration = (
@@ -400,10 +426,13 @@ const remediation = (client: SetupClient): string => {
return `Quit ${name} fully, run rea setup to refresh this registration, then start ${name} again. A running ${name} can rewrite its configuration from memory and drop the registration.`;
};
const unknownEntryPointRemediation = (): string =>
"The existing registration was retained because its executable identity cannot be verified. Run the installed REA CLI or a supported package runner, then rerun rea doctor; no registration was changed.";
const configuredStatus = (
client: SetupClient,
command: RegistrationCommand,
state: "aligned" | "stale",
state: "aligned" | "stale" | "unknown",
policyRemediation?: string,
packageReference?: string,
): ClientRegistrationStatus =>
+2
View File
@@ -7,6 +7,7 @@ import type { DirectAnalysisDependencies } from "./DirectAnalysisDependencies.js
import { providerCapabilitySet } from "../contracts/toolOutputSchemaPrimitives.js";
import { providerCapabilitySets } from "./binary/BinarySessionStatus.js";
import { sqliteDatabaseCapability } from "./sqlite/SqliteDatabaseCapability.js";
import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js";
/** Read target-free provider status and operation availability. */
export const runSessionStatus = async (
@@ -31,6 +32,7 @@ export const runSessionStatus = async (
],
server_identity: createServerIdentity({
startedAt: new Date().toISOString(),
commandPath: CURRENT_ENTRYPOINT_PATH,
}),
};
} finally {
+28 -2
View File
@@ -330,7 +330,7 @@ describe("doctor scoped readiness", () => {
client: "cursor",
config_path: "/home/user/.cursor/mcp.json",
command: ["/old/rea", "mcp"],
state: "stale",
state: "unknown",
remediation: "Run rea setup to refresh this registration.",
},
];
@@ -349,7 +349,11 @@ describe("doctor scoped readiness", () => {
expect.objectContaining({ name: "registration:codex", ok: true }),
);
expect(scoped.informational_checks).toContainEqual(
expect.objectContaining({ name: "registration:cursor", ok: false }),
expect.objectContaining({
name: "registration:cursor",
ok: false,
classification: "unknown",
}),
);
const staleSelected = await runDoctor(
@@ -591,6 +595,28 @@ describe("doctor Linux and Hopper discovery", () => {
});
describe("doctor installed Hopper discovery", () => {
it("checks the canonical user install after the system launcher", async () => {
const userPath =
"/fixture/home/Applications/Hopper Disassembler.app/Contents/MacOS/hopper";
const checked: string[] = [];
const result = await runDoctor(
undefined,
host({
homeDirectory: "/fixture/home",
executable: (candidate) => {
checked.push(candidate);
return Promise.resolve(candidate === userPath);
},
}),
);
expect(result.hopperPath).toBe(userPath);
expect(checked.slice(0, 2)).toEqual([
"/Applications/Hopper Disassembler.app/Contents/MacOS/hopper",
userPath,
]);
});
it("detects a Homebrew cask installed outside /Applications", async () => {
const path =
"/opt/homebrew/Caskroom/hopper/Hopper Disassembler.app/Contents/MacOS/hopper";
+71 -30
View File
@@ -14,6 +14,10 @@ import {
type JavaScriptRuntime,
} from "../domain/runtimeVersion.js";
import { installedCaskAppdir, probeHomebrew } from "./homebrew.js";
import {
macHopperApplicationRoots,
macHopperRenamedLauncherCandidates,
} from "./MacHopperPaths.js";
import {
linuxHopperBinarySupported,
linuxSharedLibrariesAvailable,
@@ -23,6 +27,7 @@ import {
import {
readInstalledSkillIdentity,
type InstalledSkillIdentity,
type SkillPathIssue,
} from "./SetupSkill.js";
import { CATALOG_IDENTITY } from "../catalogIdentity.js";
import { PRODUCT_IDENTITY, SDK_IDENTITY } from "../identity.js";
@@ -52,6 +57,7 @@ export interface DoctorCheck {
| "unsupported_host"
| "unsupported_target"
| "missing_analysis_engine"
| "unknown"
| "config_drift";
readonly detail?: string;
readonly details?: Readonly<Record<string, JsonValue>>;
@@ -102,8 +108,9 @@ export interface DoctorHost {
/** Validated provider settings preserved by setup, independent of readiness. */
providerRegistrationEnvironment?(): Readonly<Record<string, string>>;
installationPaths?(): Promise<readonly string[]>;
/** Undefined checks installed skill roots; explicit selections stay scoped. */
installedSkillIdentity?(
clientIds: readonly string[],
clientIds: readonly string[] | undefined,
): Promise<InstalledSkillIdentity | undefined>;
close?(): Promise<string | undefined>;
clientRegistrations?(): Promise<readonly ClientRegistrationStatus[]>;
@@ -132,6 +139,13 @@ export interface DoctorScope {
}
/** Installed-package, skill, client, and runtime identity observations. */
export type DoctorSkillState =
| "aligned"
| "stale"
| "missing"
| "unmanaged"
| "unknown";
interface DoctorIdentity {
readonly cli_package_version: string;
readonly expected_skill_version: string;
@@ -148,7 +162,8 @@ interface DoctorIdentity {
readonly skill: {
readonly installed_version: string | null;
readonly installed_tool_count: number | null;
readonly state: "aligned" | "stale" | "missing";
readonly state: DoctorSkillState;
readonly issues?: readonly SkillPathIssue[];
readonly remediation: string | null;
};
readonly registrations: readonly ClientRegistrationStatus[];
@@ -278,9 +293,7 @@ const collectDoctorIdentity = async (
const registrations = (await host.clientRegistrations?.()) ?? [];
const skillClientIds =
scope.skill_clients ??
(scope.mode === "audit-wide"
? registrations.map(({ client }) => client)
: scope.clients);
(scope.clients.length > 0 ? scope.clients : undefined);
const installedSkillIdentity =
await host.installedSkillIdentity?.(skillClientIds);
const skillAligned = skillIdentityAligned(installedSkillIdentity);
@@ -311,15 +324,13 @@ const collectDoctorIdentity = async (
skill: {
installed_version: installedSkillIdentity?.version ?? null,
installed_tool_count: installedSkillIdentity?.toolCount ?? null,
state:
installedSkillIdentity === undefined
? "missing"
: skillAligned
? "aligned"
: "stale",
state: skillIdentityState(installedSkillIdentity),
...(installedSkillIdentity?.issues === undefined
? {}
: { issues: installedSkillIdentity.issues }),
remediation: skillAligned
? null
: "Run rea setup to update the installed REA skill.",
: skillIdentityRemediation(installedSkillIdentity),
},
registrations,
},
@@ -333,25 +344,52 @@ const skillIdentityAligned = (
identity.toolCount === CATALOG_IDENTITY.counts.mcp_tools &&
identity.canonical;
const skillIdentityState = (
identity: InstalledSkillIdentity | undefined,
): DoctorSkillState => {
if (identity?.issues?.some(({ kind }) => kind === "unreadable"))
return "unknown";
if ((identity?.issues?.length ?? 0) > 0) return "unmanaged";
return identity === undefined
? "missing"
: skillIdentityAligned(identity)
? "aligned"
: "stale";
};
const skillIdentityRemediation = (
identity: InstalledSkillIdentity | undefined,
): string => {
const issues = identity?.issues;
if (issues !== undefined && issues.length > 0)
return `${issues.map(({ path, detail }) => `${detail} (${path})`).join(" ")} Verify the reported paths and use regular skill directories, or run rea setup with --skill=false to configure the client without installing the skill.`;
return "Run rea setup to update the installed REA skill.";
};
const skillIdentityCheck = (
identity: InstalledSkillIdentity | undefined,
): DoctorCheck => ({
name: "skill:identity",
ok: false,
classification: "config_drift",
detail:
identity === undefined
? "Installed REA skill identity is missing."
: "Installed REA skill identity is stale.",
remediation: "Run rea setup to update the installed REA skill.",
});
): DoctorCheck => {
const issues = identity?.issues;
return {
name: "skill:identity",
ok: false,
classification: "config_drift",
detail:
issues !== undefined && issues.length > 0
? `Selected skill identity is ${skillIdentityState(identity)}: ${issues.map(({ path }) => path).join(", ")}.`
: identity === undefined
? "Installed REA skill identity is missing."
: "Installed REA skill identity is stale.",
remediation: skillIdentityRemediation(identity),
};
};
const registrationCheck = (
registration: UnhealthyClientRegistrationStatus,
): DoctorCheck => ({
name: `registration:${registration.client}`,
ok: false,
classification: "config_drift",
classification: registration.state === "unknown" ? "unknown" : "config_drift",
detail: registration.config_path,
remediation: registration.remediation,
});
@@ -587,19 +625,22 @@ const uncomposedLinuxDemoRuntimeCheck = (): Promise<DoctorCheck> =>
});
const manualHopperPaths = async (home: string): Promise<readonly string[]> => {
const paths: string[] = [];
for (const root of ["/Applications", join(home, "Applications")]) {
const roots = macHopperApplicationRoots(home);
const bundleNamesByRoot = new Map<string, readonly string[]>();
for (const root of roots) {
try {
for (const entry of await readdir(root, { withFileTypes: true })) {
if (entry.isDirectory() && /^Hopper.*\.app$/i.test(entry.name))
paths.push(join(root, entry.name, "Contents/MacOS/hopper"));
}
bundleNamesByRoot.set(
root,
(await readdir(root, { withFileTypes: true }))
.filter((entry) => entry.isDirectory())
.map(({ name }) => name),
);
} catch (cause: unknown) {
// A missing or unreadable optional application directory is not fatal.
void cause;
}
}
return paths;
return macHopperRenamedLauncherCandidates(home, bundleNamesByRoot);
};
const uniqueLines = (value: string): string[] => [
+12 -4
View File
@@ -7,15 +7,16 @@ import {
systemLinuxHopperLauncherCandidates,
supportsReaHost,
} from "./LinuxHopper.js";
import {
MAC_HOPPER_SYSTEM_LAUNCHER,
macHopperCanonicalLauncherCandidates,
} from "./MacHopperPaths.js";
import type {
DoctorCheck,
DoctorHost,
DoctorProviderInspection,
} from "./Doctor.js";
const DEFAULT_HOPPER =
"/Applications/Hopper Disassembler.app/Contents/MacOS/hopper";
/** Ordered environment diagnostics consumed by the Doctor report assembler. */
export interface DoctorDiagnostics {
readonly checks: readonly DoctorCheck[];
@@ -150,10 +151,17 @@ const findHopper = async (host: DoctorHost): Promise<string | undefined> => {
const candidates =
host.configuredHopperPath === undefined
? [
DEFAULT_HOPPER,
...(host.platform === "darwin"
? macHopperCanonicalLauncherCandidates(host.homeDirectory)
: [MAC_HOPPER_SYSTEM_LAUNCHER]),
...(host.platform === "linux"
? linuxHopperLauncherCandidates(host.homeDirectory)
: [...systemLinuxHopperLauncherCandidates]),
...(host.platform === "darwin"
? []
: macHopperCanonicalLauncherCandidates(host.homeDirectory).slice(
1,
)),
...(await host.manualHopperPaths()),
]
: [host.configuredHopperPath];
+11 -3
View File
@@ -8,6 +8,7 @@ import type {
DoctorCheck,
DoctorProviderInspection,
DoctorScope,
DoctorSkillState,
} from "./Doctor.js";
/** Normalized readiness boundary included in every doctor response. */
@@ -29,7 +30,7 @@ interface DoctorProviderState {
interface DoctorScopeInput {
readonly checks: readonly DoctorCheck[];
readonly registrations: readonly ClientRegistrationStatus[];
readonly skillState: "aligned" | "stale" | "missing";
readonly skillState: DoctorSkillState;
readonly providers: DoctorProviderState;
readonly scope: DoctorScopeReport;
}
@@ -173,7 +174,7 @@ const registrationCheck = (
): DoctorCheck => ({
name: `registration:${status.client}`,
ok: false,
classification: "config_drift",
classification: status.state === "unknown" ? "unknown" : "config_drift",
detail: status.config_path,
remediation: status.remediation,
});
@@ -188,7 +189,14 @@ const selectedSkillCheck = (
classification: state === "aligned" ? "healthy" : "config_drift",
...(state === "aligned"
? { detail: "Installed REA skill identity is aligned." }
: { remediation: "Run rea setup to update the installed REA skill." }),
: {
remediation:
state === "unmanaged"
? "Use regular skill directories, or run rea setup with --skill=false to configure the client without installing the skill."
: state === "unknown"
? "Verify access to the reported skill files before retrying."
: "Run rea setup to update the installed REA skill.",
}),
};
const doctorProviderCheckName = (providerId: string, name: string): boolean =>
+147 -42
View File
@@ -47,6 +47,49 @@ import { resolveProcedureAddress } from "./ProcedureAddressResolution.js";
import { traceLiteralFeature } from "./EnhancedLiteralTracing.js";
import { projectNativeApiInspection } from "./native/NativeApiInspection.js";
type BatchProcedureIdentity =
| {
readonly status: "resolved";
readonly address: string;
readonly name: string | null;
readonly name_error?: ReturnType<typeof projectAnalysisError>;
}
| {
readonly status: "unknown";
readonly error: ReturnType<typeof projectAnalysisError>;
};
type BatchResolution = {
readonly address: string;
readonly result: Result<string, AnalysisError>;
};
type BatchReadyItem = {
readonly address: string;
readonly procedure: BatchProcedureIdentity;
readonly providerProcedure: string;
};
type BatchDecompileItem = {
readonly address: string;
readonly procedure: BatchProcedureIdentity;
readonly status: "ok" | "error";
readonly pseudocode?: string;
readonly error?: ReturnType<typeof projectAnalysisError>;
};
const isAnalysisCancellation = (
error: AnalysisError,
): error is AnalysisCancelledError => error._tag === "AnalysisCancelledError";
const firstAnalysisCancellation = <Value>(
results: readonly Result<Value, AnalysisError>[],
): AnalysisCancelledError | undefined => {
for (const result of results)
if (!result.ok && isAnalysisCancellation(result.error)) return result.error;
return undefined;
};
/**
* Composes direct provider operations into bounded reverse-engineering tools.
* Direct callers use `execute`; adapters that own validation use
@@ -259,52 +302,34 @@ export class EnhancedTools {
addresses: readonly string[],
signal?: AbortSignal,
): EnhancedResult {
const items = await Promise.all(
const resolutions = await Promise.all(
addresses.map(async (address) => {
const resolved = await resolveProcedureAddress(
const result = await resolveProcedureAddress(
this.#call.bind(this),
address,
signal,
);
const procedure = resolved.ok
? await this.#batchProcedureIdentity(resolved.value, signal)
: {
status: "unknown" as const,
error: projectAnalysisError(resolved.error),
};
const result = await this.#call(
"procedure_pseudo_code",
{ procedure: resolved.ok ? resolved.value : address },
signal,
resolved.ok ? "provider" : undefined,
);
if (!result.ok)
return {
address,
procedure,
status: "error" as const,
error: projectAnalysisError(result.error),
};
if (typeof result.value !== "string" || result.value.length === 0)
return {
address,
procedure,
status: "error" as const,
error: projectAnalysisError(
new AnalysisOutputError(
"procedure_pseudo_code",
"provider returned empty pseudocode",
),
),
};
return {
address,
procedure,
status: "ok" as const,
pseudocode: result.value,
};
return { address, result };
}),
);
const resolutionCancellation = firstAnalysisCancellation(
resolutions.map(({ result }) => result),
);
if (resolutionCancellation !== undefined)
return err(resolutionCancellation);
const identities = await this.#batchIdentities(resolutions, signal);
const ready: BatchReadyItem[] = [];
for (const identity of identities) {
if (!identity.ok) return err(identity.error);
ready.push(identity.value);
}
const decompilations = await this.#batchPseudocode(ready, signal);
const items: BatchDecompileItem[] = [];
for (const decompilation of decompilations) {
if (!decompilation.ok) return err(decompilation.error);
items.push(decompilation.value);
}
const succeeded = items.filter(({ status }) => status === "ok").length;
return ok({
items,
@@ -314,12 +339,91 @@ export class EnhancedTools {
});
}
async #batchProcedureIdentity(address: string, signal?: AbortSignal) {
async #batchIdentities(
resolutions: readonly BatchResolution[],
signal?: AbortSignal,
): Promise<Result<BatchReadyItem, AnalysisCancelledError>[]> {
return Promise.all(
resolutions.map(async ({ address, result }) => {
if (!result.ok)
return ok({
address,
providerProcedure: address,
procedure: {
status: "unknown" as const,
error: projectAnalysisError(result.error),
},
});
const identity = await this.#batchProcedureIdentity(
result.value,
signal,
);
return identity.ok
? ok({
address,
providerProcedure: result.value,
procedure: identity.value,
})
: err(identity.error);
}),
);
}
async #batchPseudocode(
ready: readonly BatchReadyItem[],
signal?: AbortSignal,
): Promise<Result<BatchDecompileItem, AnalysisCancelledError>[]> {
return Promise.all(
ready.map(async ({ address, procedure, providerProcedure }) => {
const result = await this.#call(
"procedure_pseudo_code",
{ procedure: providerProcedure },
signal,
procedure.status === "resolved" ? "provider" : undefined,
);
if (!result.ok && isAnalysisCancellation(result.error))
return err(result.error);
const item: BatchDecompileItem = !result.ok
? {
address,
procedure,
status: "error",
error: projectAnalysisError(result.error),
}
: typeof result.value !== "string" || result.value.length === 0
? {
address,
procedure,
status: "error",
error: projectAnalysisError(
new AnalysisOutputError(
"procedure_pseudo_code",
"provider returned empty pseudocode",
),
),
}
: {
address,
procedure,
status: "ok",
pseudocode: result.value,
};
return ok(item);
}),
);
}
async #batchProcedureIdentity(
address: string,
signal?: AbortSignal,
): Promise<Result<BatchProcedureIdentity, AnalysisCancelledError>> {
const observed = await this.#call("address_name", { address }, signal);
if (!observed.ok && isAnalysisCancellation(observed.error))
return err(observed.error);
const named =
observed.ok &&
(typeof observed.value === "string" || observed.value === null);
return {
return ok({
status: "resolved" as const,
address,
name: named ? observed.value : null,
@@ -335,7 +439,7 @@ export class EnhancedTools {
: observed.error,
),
}),
};
});
}
async #callGraph(
@@ -385,6 +489,7 @@ export class EnhancedTools {
"provider",
);
if (!result.ok) {
if (isAnalysisCancellation(result.error)) return result;
graph[level].push({
address: current.address,
status: "error",
+22 -7
View File
@@ -1,4 +1,4 @@
import { realpath } from "node:fs/promises";
import { realpath, stat } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { stripComments } from "jsonc-parser";
import { z } from "zod";
@@ -149,12 +149,27 @@ export const readClientPolicyBlock = async (
".gemini",
"settings.json",
);
const document = await readConfigurationFile(path, "json", resources);
if (!document.ok) return document;
if (document.value !== undefined) {
const trusted = await workspaceTrusted(settings, sources, resources);
if (!trusted.ok) return trusted;
if (trusted.value) {
// Only ENOENT proves absence; preserve other metadata failures as unreadable.
const present = await stat(path).then(
() => ok(true),
(cause: unknown) =>
cause instanceof Error && "code" in cause && cause.code === "ENOENT"
? ok(false)
: err({
kind: "unreadable" as const,
path,
detail: `Configuration ${path} could not be read: ${cause instanceof Error ? cause.message : String(cause)}`,
}),
);
if (!present.ok) return present;
const includeWorkspace = present.value
? await workspaceTrusted(settings, sources, resources)
: ok(false);
if (!includeWorkspace.ok) return includeWorkspace;
if (includeWorkspace.value) {
const document = await readConfigurationFile(path, "json", resources);
if (!document.ok) return document;
if (document.value !== undefined) {
const source = { path, document: document.value };
const systemIndex = sources.findIndex(
(item) => item.path === settings.systemPath,
+2 -2
View File
@@ -17,6 +17,7 @@ import { z } from "zod";
import { homeDirectoryFromEnvironment } from "../config/homeDirectory.js";
import { HOPPER_PUBLISHER } from "../domain/hopperPublisher.js";
import { macDeveloperIdRequirement } from "../process/MacCodeSignature.js";
import { macHopperUserApplicationPath } from "./MacHopperPaths.js";
import {
downloadHopperPackage,
HOPPER_DOWNLOAD_BYTES,
@@ -320,8 +321,7 @@ export const systemMacHopperInstallHost = (
}
return failure;
},
destination: () =>
join(homeDirectory, "Applications/Hopper Disassembler.app"),
destination: () => macHopperUserApplicationPath(homeDirectory),
};
};
+56
View File
@@ -0,0 +1,56 @@
import { mkdtemp, mkdir, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
macHopperCanonicalLauncherCandidates,
macHopperLauncherCandidates,
macHopperRenamedLauncherCandidates,
} from "./MacHopperPaths.js";
describe("macOS Hopper launcher paths", () => {
const home = "/fixture/home";
it("keeps canonical system and user candidates ahead of renamed bundles", () => {
expect(macHopperCanonicalLauncherCandidates(home)).toEqual([
"/Applications/Hopper Disassembler.app/Contents/MacOS/hopper",
"/fixture/home/Applications/Hopper Disassembler.app/Contents/MacOS/hopper",
]);
expect(
macHopperRenamedLauncherCandidates(
home,
new Map([
[
"/Applications",
["Hopper z.app", "Hopper a.app", "Hopper Disassembler.app"],
],
["/fixture/home/Applications", ["Hopper User.app"]],
]),
),
).toEqual([
"/Applications/Hopper a.app/Contents/MacOS/hopper",
"/Applications/Hopper z.app/Contents/MacOS/hopper",
"/fixture/home/Applications/Hopper User.app/Contents/MacOS/hopper",
]);
});
it("always includes the installer's canonical user destination", () => {
expect(macHopperLauncherCandidates(home)).toContain(
"/fixture/home/Applications/Hopper Disassembler.app/Contents/MacOS/hopper",
);
});
it("finds renamed user bundles through the runtime filesystem scan", async () => {
const temporary = await mkdtemp(join(tmpdir(), "rea-hopper-paths-"));
const appDirectory = join(temporary, "Applications", "Hopper Preview.app");
try {
await mkdir(appDirectory, { recursive: true });
expect([...macHopperLauncherCandidates(temporary)]).toContain(
join(appDirectory, "Contents/MacOS/hopper"),
);
} finally {
await rm(temporary, { recursive: true, force: true });
}
});
});
+76
View File
@@ -0,0 +1,76 @@
import { readdirSync } from "node:fs";
import { join } from "node:path";
export const MAC_HOPPER_APPLICATION_DIRECTORY = "/Applications";
export const MAC_HOPPER_APPLICATION_NAME = "Hopper Disassembler.app";
export const MAC_HOPPER_USER_APPLICATIONS_DIRECTORY = "Applications";
export const MAC_HOPPER_SYSTEM_LAUNCHER = join(
MAC_HOPPER_APPLICATION_DIRECTORY,
MAC_HOPPER_APPLICATION_NAME,
"Contents/MacOS/hopper",
);
export const macHopperUserApplicationPath = (homeDirectory: string): string =>
join(
homeDirectory,
MAC_HOPPER_USER_APPLICATIONS_DIRECTORY,
MAC_HOPPER_APPLICATION_NAME,
);
export const macHopperApplicationRoots = (
homeDirectory: string,
): readonly string[] => [
MAC_HOPPER_APPLICATION_DIRECTORY,
join(homeDirectory, MAC_HOPPER_USER_APPLICATIONS_DIRECTORY),
];
export const macHopperCanonicalLauncherCandidates = (
homeDirectory: string,
): readonly string[] => [
MAC_HOPPER_SYSTEM_LAUNCHER,
join(macHopperUserApplicationPath(homeDirectory), "Contents/MacOS/hopper"),
];
/** Build deterministic candidates from the discovered app bundle names. */
export const macHopperRenamedLauncherCandidates = (
homeDirectory: string,
bundleNamesByRoot: ReadonlyMap<string, readonly string[]>,
): readonly string[] =>
macHopperApplicationRoots(homeDirectory).flatMap((root) =>
[...new Set(bundleNamesByRoot.get(root) ?? [])]
.filter(
(name) =>
/^Hopper.*\.app$/iu.test(name) &&
name.toLocaleLowerCase("en-US") !==
MAC_HOPPER_APPLICATION_NAME.toLocaleLowerCase("en-US"),
)
.sort()
.map((name) => join(root, name, "Contents/MacOS/hopper")),
);
const localBundleNames = (root: string): readonly string[] => {
try {
return readdirSync(root, { withFileTypes: true })
.filter(
(entry) => entry.isDirectory() && /^Hopper.*\.app$/iu.test(entry.name),
)
.map(({ name }) => name);
} catch (cause: unknown) {
// Missing or unreadable optional app directories contain no candidates.
void cause;
return [];
}
};
/** Yield canonical paths first; enumerate renamed bundles only if needed. */
export function* macHopperLauncherCandidates(
homeDirectory: string,
): Generator<string> {
yield* macHopperCanonicalLauncherCandidates(homeDirectory);
for (const root of macHopperApplicationRoots(homeDirectory))
yield* macHopperRenamedLauncherCandidates(
homeDirectory,
new Map([[root, localBundleNames(root)]]),
);
}
@@ -2,6 +2,7 @@ import { z } from "zod";
import { AnalysisProtocolError } from "../domain/analysisErrorCore.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { NonCanonicalEvidenceBundleError } from "../domain/evidenceBundle.js";
import { createEvidence, type Evidence } from "../domain/evidence.js";
import { jsonObjectSchema, jsonValueSchema } from "../domain/jsonValue.js";
import {
@@ -45,6 +46,8 @@ export const buildReconstructionObligationLedgerEvidenceValidated = (
});
return ok(createLedgerEvidence(input, ledger));
} catch (cause: unknown) {
if (cause instanceof NonCanonicalEvidenceBundleError)
return err(workflowInputError(OPERATION, cause));
if (cause instanceof z.ZodError)
return err(workflowInputError(OPERATION, cause));
return err(
@@ -6,6 +6,64 @@ import { parseReferenceSourceEntries } from "./ReferenceSourceImportEntries.js";
import { historicalSourceLanguages } from "../domain/referenceSourceGraph.js";
import type { ReferenceSourceRead } from "../reference/ReferenceSourceReaderTypes.js";
it("projects an unsupported non-regular entry as unknown kind instead of file", () => {
const read: ReferenceSourceRead = {
root: "/selected/source",
entries: [
{
status: "failed",
kind: "other",
path: "src/pipe",
code: "unsupported",
message: "Entry is not a regular file",
size: 0,
},
],
bytesRead: 0,
limitations: [],
};
const parsed = parseReferenceSourceEntries(read, new Set());
expect(parsed.entries).toContainEqual({
path: "src/pipe",
kind: "unknown",
classifications: ["unknown"],
limitations: [
"Entry is not a regular file Exclude this entry or replace it with a regular file.",
],
});
expect(parsed.entries.some((entry) => entry.kind === "file")).toBe(false);
});
it("keeps a failed regular file as file kind", () => {
const read: ReferenceSourceRead = {
root: "/selected/source",
entries: [
{
status: "failed",
kind: "file",
path: "src/broken.ts",
code: "io",
message: "Entry could not be read",
},
],
bytesRead: 0,
limitations: [],
};
const parsed = parseReferenceSourceEntries(read, new Set());
expect(parsed.entries).toContainEqual({
path: "src/broken.ts",
kind: "file",
sha256: null,
size: null,
language: null,
classifications: ["source"],
content_state: "unreadable",
limitations: [
"Entry could not be read This file could not be read. Check its permissions, then try again.",
],
});
});
it("retains Kotlin source identity and language across the inventory projection", () => {
const files = [
[
@@ -75,7 +75,7 @@ const failedEntry = (
classifications,
limitations: [limitation],
};
if (entry.kind === "unknown")
if (entry.kind === "unknown" || entry.kind === "other")
return {
path: entry.path,
kind: "unknown",
+8 -7
View File
@@ -1,9 +1,10 @@
import { describe, expect, it } from "vitest";
import { join, resolve } from "node:path";
import { join } from "node:path";
import { FakeSetupHost, options } from "./Setup.fixture.js";
import { runSetup } from "./Setup.js";
import { setupRegistrationCommand } from "./SetupHost.js";
import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js";
import { discoverSetupState } from "./SetupPlan.js";
import type { MsixPackageView } from "./MsixPackageStorage.js";
import type { SetupClient } from "./SupportedClients.js";
@@ -333,13 +334,13 @@ describe("setup workflow action selection", () => {
expect(result.status).toBe("ready");
expect(host.configuredCommands).toEqual([
setupRegistrationCommand("win32", undefined),
]);
expect(setupRegistrationCommand("win32", undefined)).toEqual([
process.execPath,
resolve(process.argv[1] ?? "rea"),
"mcp",
setupRegistrationCommand("win32", undefined, CURRENT_ENTRYPOINT_PATH),
]);
expect(
setupRegistrationCommand("win32", undefined, CURRENT_ENTRYPOINT_PATH),
).toEqual([process.execPath, CURRENT_ENTRYPOINT_PATH, "mcp"]);
expect(setupRegistrationCommand("win32", undefined, undefined)).toEqual([]);
expect(setupRegistrationCommand("win32", "npx", undefined)).not.toEqual([]);
});
it.each([
+15 -3
View File
@@ -8,7 +8,7 @@ import type {
} from "./SetupTypes.js";
import type { SetupClient } from "./SupportedClients.js";
import type { SetupHopperInstallResult } from "./SetupInstallFailure.js";
import { err, ok } from "../domain/result.js";
import { err, ok, type Result } from "../domain/result.js";
import { SetupSkillInstallError } from "./SetupSkill.js";
import type { DoctorCheck, DoctorReport, DoctorScope } from "./Doctor.js";
import type { LinuxDistribution } from "./LinuxHopper.js";
@@ -16,6 +16,7 @@ import { PRODUCT_IDENTITY, SDK_IDENTITY } from "../identity.js";
import { CATALOG_IDENTITY } from "../catalogIdentity.js";
import type { ClientRegistrationStatus } from "./ClientRegistrationStatus.js";
import { setupRegistrationCommand } from "./SetupHost.js";
import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js";
/** Recording setup host for service-level planning and recovery tests. */
export class FakeSetupHost implements SetupHost {
@@ -24,7 +25,7 @@ export class FakeSetupHost implements SetupHost {
skillDestinations(clientIds: readonly string[]) {
return skillDestinations(this.homeDirectory, clientIds);
}
readonly registrationCommand: readonly string[];
registrationCommand: readonly string[];
registrationCommandBlocker?: string;
nodeVersion = "24.18.0";
version: string | undefined = "14.5";
@@ -43,6 +44,7 @@ export class FakeSetupHost implements SetupHost {
hopperReplaceRequests: boolean[] = [];
configurations = 0;
skillInstalls = 0;
skillDirectoryValidations: Array<readonly string[]> = [];
doctorCalls = 0;
doctorScopes: Array<DoctorScope | undefined> = [];
checkedHopperPaths: Array<string | undefined> = [];
@@ -56,7 +58,11 @@ export class FakeSetupHost implements SetupHost {
constructor(platform: NodeJS.Platform = "darwin") {
this.platform = platform;
this.registrationCommand = setupRegistrationCommand(platform, undefined);
this.registrationCommand = setupRegistrationCommand(
platform,
undefined,
CURRENT_ENTRYPOINT_PATH,
);
}
macosVersion = (): Promise<string | undefined> =>
@@ -129,6 +135,12 @@ export class FakeSetupHost implements SetupHost {
);
skillNeedsInstall = (): Promise<boolean> =>
Promise.resolve(this.skill !== "unchanged");
validateSkillDirectories = (
clientIds: readonly string[],
): Promise<Result<void, SetupSkillInstallError>> => {
this.skillDirectoryValidations.push(clientIds);
return Promise.resolve(ok(undefined));
};
installSkill = () => {
this.skillInstalls += 1;
return Promise.resolve(
+140
View File
@@ -3,6 +3,97 @@ import { describe, expect, it } from "vitest";
import { runSetup } from "./Setup.js";
import type { SetupProgressEvent } from "./SetupTypes.js";
import { FakeSetupHost, options } from "./Setup.fixture.js";
import { SetupSkillInstallError } from "./SetupSkill.js";
import { err, ok } from "../domain/result.js";
const linkedSkillFailure = () =>
new SetupSkillInstallError({
path: "/fixture/home/.claude/skills/reverse-engineer-anything",
stage: "prepare",
reason: "Skill directory is a symbolic link",
rollbackFailures: [],
recoveryBackupPaths: [],
cause: undefined,
});
describe("selected skill admission", () => {
it.each(["codex", "shared"] as const)(
"keeps %s choices when an unselected client is linked",
async (choice) => {
const host = new FakeSetupHost();
host.hopper = "/Applications/Hopper";
host.clients = [
{ name: "claude_code", configPath: "/claude.json" },
{ name: "codex", configPath: "/codex.toml", format: "toml" },
];
host.validateSkillDirectories = async (ids) => {
host.skillDirectoryValidations.push(ids);
return ids.includes("claude_code")
? err(linkedSkillFailure())
: ok(undefined);
};
const result = await runSetup(
{ ...options(false), structured: false },
host,
async (actions, context) => {
if (context?.stage === "select") {
expect(actions.some(({ kind }) => kind === "install_skill")).toBe(
true,
);
return {
approved: false,
selectedActionIds:
choice === "codex"
? ["configure_client:codex"]
: ["install_skill"],
};
}
return true;
},
);
expect(result.status).toBe("ready");
expect(host.skillInstalls).toBe(1);
expect(host.configurations).toBe(choice === "codex" ? 1 : 0);
expect(host.skillDirectoryValidations).toEqual(
choice === "codex" ? [["codex"], ["codex"]] : [[], []],
);
},
);
it("rechecks directory admission after approval before changing clients or Hopper", async () => {
const host = new FakeSetupHost("linux");
host.distribution = {
id: "ubuntu",
versionId: "24.04",
packageFamily: "deb",
hopperSetupSupported: true,
};
host.clients = [{ name: "claude_code", configPath: "/claude.json" }];
let linked = false;
host.validateSkillDirectories = async () =>
linked ? err(linkedSkillFailure()) : ok(undefined);
const result = await runSetup(
{
...options(false, true),
clientIds: ["claude_code"],
structured: false,
},
host,
async () => {
linked = true;
return true;
},
);
expect(result).toMatchObject({
status: "needs_human",
code: "skill_install_failed",
appliedActions: [],
});
expect(host.configurations).toBe(0);
expect(host.hopperInstalls).toBe(0);
expect(host.skillInstalls).toBe(0);
});
});
describe("skill plan destinations", () => {
it("plans a Claude-only skill in its personal location", async () => {
@@ -45,6 +136,55 @@ describe("skill plan destinations", () => {
});
describe("setup workflow", () => {
it("blocks client registration when no executable entrypoint is available", async () => {
const host = new FakeSetupHost("win32");
host.registrationCommand = [];
host.registrationCommandBlocker =
"REA could not determine the current executable entrypoint.";
host.clients = [{ name: "codex", configPath: "C:\\config\\codex.toml" }];
const result = await runSetup(
{ ...options(true), clientIds: ["codex"], installSkill: false },
host,
);
expect(result.status).toBe("needs_human");
expect(result.plannedActions).toEqual([]);
expect(result.remediation).toBe(host.registrationCommandBlocker);
expect(host.configurations).toBe(0);
});
it("keeps a relative Node registration out of setup defaults when cwd happens to match", async () => {
const host = new FakeSetupHost();
host.clients = [{ name: "cursor", configPath: "/cursor.json" }];
host.productRegistrations = [
{
client: "cursor",
config_path: "/cursor.json",
command: [process.execPath, "./rea.mjs", "mcp"],
state: "stale",
remediation: "Run rea setup to refresh this registration.",
},
];
const result = await runSetup(
{ ...options(false), clientIds: [], installSkill: false },
host,
);
expect(result.clientStates).toContainEqual(
expect.objectContaining({
client: expect.objectContaining({ name: "cursor" }),
status: "unknown",
configured: false,
}),
);
expect(result.plannedActions).toEqual([]);
expect(host.configurations).toBe(0);
});
});
describe("setup plan application", () => {
it("returns a complete plan without mutation", async () => {
const host = new FakeSetupHost();
host.clients = [{ name: "cursor", configPath: "/cursor.json" }];
+20 -4
View File
@@ -99,6 +99,12 @@ const runSetupWorkflow = async (
[host, plannedActions, appliedActions, clients, clientStates],
readinessScope,
);
const failSkill = async (
error: SetupSkillInstallError,
): Promise<SetupResult> => ({
...(await fail(error.remediation)),
code: "skill_install_failed",
});
const unsupported = await hostRemediation(host, false);
if (unsupported !== undefined) return fail(unsupported);
const initialState = await host.initialSetupState(options.readinessScope);
@@ -235,6 +241,12 @@ const runSetupWorkflow = async (
);
const manualRemediation = manualClientsRemediation(planned.selectedClients);
if (skillSelected) {
const admission = await host.validateSkillDirectories(
selectedSkillClientIds,
);
if (!admission.ok) return failSkill(admission.error);
}
if (options.dryRun === true)
return {
status: "planned",
@@ -285,6 +297,13 @@ const runSetupWorkflow = async (
: {}),
};
// Approval may pause while a skill manager changes a selected directory.
if (planSelection.skillSelected) {
const admission = await host.validateSkillDirectories(
selectedSkillClientIds,
);
if (!admission.ok) return failSkill(admission.error);
}
let selectedClients: readonly SetupClient[] =
planSelection.clientsToConfigure;
if (
@@ -325,10 +344,7 @@ const runSetupWorkflow = async (
selectedSkillClientIds,
);
if (skillFailure !== undefined) {
return {
...(await fail(skillFailure.remediation)),
code: "skill_install_failed",
};
return failSkill(skillFailure);
}
}
const doctor = summarizeDoctor(await host.doctor(readinessScope));
+27 -16
View File
@@ -31,6 +31,7 @@ import {
skillDestinations,
canonicalSkillNeedsInstall,
installCanonicalSkill,
validateCanonicalSkillDirectories,
} from "./SetupSkill.js";
import {
clientConfigurationAligned,
@@ -55,21 +56,21 @@ import type {
SetupProviderEnvironment,
} from "./SetupTypes.js";
import type { DoctorScope } from "./Doctor.js";
import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js";
/** Resolve the executable and arguments used in a managed MCP registration. */
export const setupRegistrationCommand = (
platform: NodeJS.Platform,
packageRunner: PackageRunner | undefined,
): readonly string[] =>
packageRunner !== undefined
? packageRunnerRegistrationCommand(packageRunner, platform)
: platform === "win32"
? [
process.execPath,
resolve(process.argv[1] ?? PRODUCT_IDENTITY.cliBinary),
"mcp",
]
: [resolve(process.argv[1] ?? PRODUCT_IDENTITY.cliBinary), "mcp"];
entrypointPath: string | undefined,
): readonly string[] => {
if (packageRunner !== undefined)
return packageRunnerRegistrationCommand(packageRunner, platform);
if (entrypointPath === undefined || entrypointPath.length === 0) return [];
return platform === "win32"
? [process.execPath, entrypointPath, "mcp"]
: [entrypointPath, "mcp"];
};
/**
* The launcher setup registers, and why it must not be registered when it
@@ -83,14 +84,17 @@ const setupRegistration = (
const registrationCommand = setupRegistrationCommand(
platform,
invokingPackageRunner(environment, platform),
CURRENT_ENTRYPOINT_PATH,
);
const registrationCommandBlocker =
platform === "win32"
? packagePrivateLauncherBlocker(
registrationCommand,
msixPackagesDirectory(homeDirectory, environment),
)
: undefined;
registrationCommand.length === 0
? "REA could not determine the current executable entrypoint. Run setup through the installed REA CLI or a supported package runner before configuring clients."
: platform === "win32"
? packagePrivateLauncherBlocker(
registrationCommand,
msixPackagesDirectory(homeDirectory, environment),
)
: undefined;
return registrationCommandBlocker === undefined
? { registrationCommand }
: { registrationCommand, registrationCommandBlocker };
@@ -224,6 +228,13 @@ export const systemSetupHost = (
platform,
resources,
),
validateSkillDirectories: (clientIds) =>
validateCanonicalSkillDirectories(
homeDirectory,
clientIds,
environment,
platform,
),
installSkill: (clientIds) =>
installCanonicalSkill(
homeDirectory,
+34 -15
View File
@@ -1,12 +1,11 @@
import { join } from "node:path";
import { isOwnedClientRegistrationCommand } from "./ClientRegistrationIdentity.js";
import { classifyClientRegistrationCommand } from "./ClientRegistrationIdentity.js";
import {
linuxHopperInstallDisclosure,
type LinuxPackageFamily,
} from "./LinuxHopper.js";
import type { SkillDestination } from "./SetupSkill.js";
import { macHopperInstallDisclosure } from "./MacHopper.js";
import { macHopperUserApplicationPath } from "./MacHopperPaths.js";
import type {
ClientConfigurationInspection,
SetupAction,
@@ -22,6 +21,7 @@ import {
import { redirectedWriteBlocker } from "./MsixPackageStorage.js";
import type { DoctorReport } from "./Doctor.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js";
/** Read-only setup facts shared by client selection and preflight. */
export interface SetupDiscovery {
@@ -78,20 +78,29 @@ export const discoverSetupState = async (input: {
);
const clientStates = clients.map((client): SetupClientState => {
const registration = registrationByClient.get(client.name);
const commandIdentity =
registration === undefined
? undefined
: classifyClientRegistrationCommand(
registration.command,
CURRENT_ENTRYPOINT_PATH,
);
const ownedRegistration =
(registration?.state === "aligned" || registration?.state === "stale") &&
isOwnedClientRegistrationCommand(registration.command);
commandIdentity === "owned";
const configured = ownedRegistration;
const status: SetupClientState["status"] =
registration?.state === "invalid"
? "invalid"
: ownedRegistration && registration.state === "aligned"
? "configured"
: ownedRegistration && registration.state === "stale"
? "needs_configuration"
: detectedIds.has(client.name)
: registration?.state === "unknown" || commandIdentity === "unknown"
? "unknown"
: ownedRegistration && registration.state === "aligned"
? "configured"
: ownedRegistration && registration.state === "stale"
? "needs_configuration"
: "missing";
: detectedIds.has(client.name)
? "needs_configuration"
: "missing";
return {
client,
detected: detectedIds.has(client.name),
@@ -103,7 +112,10 @@ export const discoverSetupState = async (input: {
.filter(
(registration) =>
(registration.state === "aligned" || registration.state === "stale") &&
isOwnedClientRegistrationCommand(registration.command),
classifyClientRegistrationCommand(
registration.command,
CURRENT_ENTRYPOINT_PATH,
) === "owned",
)
.map(({ client }) => client);
return {
@@ -150,6 +162,16 @@ export const planSetupActions = async (input: {
const client = byId.get(id);
return client === undefined ? [] : [client];
});
if (
selectedClients.length > 0 &&
input.host.registrationCommandBlocker !== undefined
)
return {
plannedActions: [],
selectedClients,
clientsToConfigure: [],
blocker: input.host.registrationCommandBlocker,
};
const inspections = await Promise.all(
selectedClients.map(async (client) => ({
client,
@@ -264,10 +286,7 @@ const setupPlan = (input: {
label: "Hopper deep-analysis provider",
target:
input.platform === "darwin"
? join(
input.homeDirectory,
"Applications/Hopper Disassembler.app",
)
? macHopperUserApplicationPath(input.homeDirectory)
: "system package manager",
detail:
input.platform === "linux"
+228 -70
View File
@@ -178,6 +178,14 @@ export interface InstalledSkillIdentity {
readonly version: string | null;
readonly toolCount: number | null;
readonly canonical: boolean;
readonly issues?: readonly SkillPathIssue[];
}
/** Known management blockers and unresolved filesystem observations. */
export interface SkillPathIssue {
readonly path: string;
readonly kind: "symbolic_link" | "not_directory" | "unreadable";
readonly detail: string;
}
/** Resolve the skill roots required by a client selection, or all owned roots. */
@@ -209,8 +217,10 @@ export const existingSkillDestinations = async (
platform,
)) {
try {
const root = await lstat(destination.path);
if (root.isSymbolicLink() || !root.isDirectory()) continue;
const issues = await skillDirectoryIssues([destination]);
if (issues.some(({ kind }) => kind === "unreadable"))
throw skillDirectoryFailure(issues);
if (issues.length > 0) continue;
const manifest = join(destination.path, "SKILL.md");
const content = await readSkillFile(manifest, resources);
if (isManagedSkillManifest(content)) existing.push(destination);
@@ -242,49 +252,212 @@ const readOptionalSkillState = async (
}
};
const inspectSkillDirectory = async (
path: string,
): Promise<Result<void, SkillPathIssue>> => {
try {
const directory = await lstat(path);
if (directory.isSymbolicLink())
return err({
path,
kind: "symbolic_link",
detail: "Skill directory is a symbolic link.",
});
if (!directory.isDirectory())
return err({
path,
kind: "not_directory",
detail: "Skill path is not a directory.",
});
return ok(undefined);
} catch (cause: unknown) {
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
return ok(undefined);
return err({
path,
kind: "unreadable",
detail: cause instanceof Error ? cause.message : String(cause),
});
}
};
const skillDirectoryIssues = async (
destinations: readonly SkillDestination[],
): Promise<readonly SkillPathIssue[]> => {
const issues: SkillPathIssue[] = [];
for (const { path } of destinations) {
const root = await inspectSkillDirectory(path);
if (!root.ok) {
issues.push(root.error);
continue;
}
const references = await inspectSkillDirectory(join(path, "references"));
if (!references.ok) issues.push(references.error);
}
return issues;
};
const skillDirectoryFailure = (
issues: readonly SkillPathIssue[],
): SetupSkillInstallError =>
new SetupSkillInstallError({
path: issues[0]?.path ?? "skill bundle",
stage: "prepare",
reason: `${issues.map(({ path, detail }) => `${detail} (${path})`).join(" ")} Use a regular skill directory, or run setup with --skill=false to configure the client without installing the skill`,
rollbackFailures: [],
recoveryBackupPaths: [],
cause: issues,
});
/** Validate only final selected roots; permission to install is separate from freshness. */
export const validateCanonicalSkillDirectories = async (
home: string,
clientIds: readonly string[],
environment: Readonly<NodeJS.ProcessEnv> = {},
platform: NodeJS.Platform = process.platform,
): Promise<Result<void, SetupSkillInstallError>> => {
const issues = await skillDirectoryIssues(
skillDestinations(home, clientIds, environment, platform),
);
return issues.length === 0
? ok(undefined)
: err(skillDirectoryFailure(issues));
};
const canonicalSkillFiles = async (
home: string,
clientIds: readonly string[],
environment: Readonly<NodeJS.ProcessEnv> = {},
platform: NodeJS.Platform = process.platform,
resources?: ArtifactResourceScope,
): Promise<readonly CanonicalSkillFile[]> =>
Promise.all(
skillDestinations(home, clientIds, environment, platform).flatMap(
({ path: root }) =>
MANAGED_SKILL_FILES.map(async (relativePath) => {
const destination = join(root, relativePath);
return {
destination,
content: await canonicalSkillContent(relativePath),
original: await readOptionalSkillState(destination, resources),
};
}),
),
);
const canonicalSkillNeedsInstallByReading = async (
home: string,
clientIds: readonly string[],
environment: Readonly<NodeJS.ProcessEnv>,
platform: NodeJS.Platform,
resources?: ArtifactResourceScope,
): Promise<boolean> => {
for (const { path: root } of skillDestinations(
): Promise<readonly CanonicalSkillFile[]> => {
const destinations = skillDestinations(
home,
clientIds,
environment,
platform,
)) {
);
const admission = await validateCanonicalSkillDirectories(
home,
clientIds,
environment,
platform,
);
if (!admission.ok) throw admission.error;
return Promise.all(
destinations.flatMap(({ path: root }) =>
MANAGED_SKILL_FILES.map(async (relativePath) => {
const destination = join(root, relativePath);
return {
destination,
content: await canonicalSkillContent(relativePath),
original: await readOptionalSkillState(destination, resources),
};
}),
),
);
};
interface CanonicalSkillInspection {
readonly canonical: boolean;
readonly manifest: string | undefined;
readonly installedDestination: boolean;
}
interface SkillInspectionFailure {
readonly _tag: "SkillInspectionFailure";
readonly issues: readonly SkillPathIssue[];
readonly cause: unknown;
readonly manifest: string | undefined;
}
const inspectCanonicalSkill = async (
home: string,
clientIds: readonly string[] | undefined,
environment: Readonly<NodeJS.ProcessEnv>,
platform: NodeJS.Platform,
resources?: ArtifactResourceScope,
): Promise<Result<CanonicalSkillInspection, SkillInspectionFailure>> => {
const destinations = skillDestinations(
home,
clientIds,
environment,
platform,
);
const issues =
clientIds === undefined
? []
: [...(await skillDirectoryIssues(destinations))];
if (issues.length > 0)
return err({
_tag: "SkillInspectionFailure",
issues,
cause: issues,
manifest: undefined,
});
let canonical = true;
let manifest: string | undefined;
let failureCause: unknown;
let installedDestination = false;
for (const destination of destinations) {
const { path: root } = destination;
const pathIssues =
clientIds === undefined ? await skillDirectoryIssues([destination]) : [];
if (pathIssues.length > 0) {
issues.push(...pathIssues);
if (failureCause === undefined) failureCause = pathIssues;
installedDestination = true;
canonical = false;
continue;
}
let rootCanonical = true;
let rootHasFiles = false;
const rootIssues: SkillPathIssue[] = [];
for (const relativePath of MANAGED_SKILL_FILES) {
const destination = join(root, relativePath);
const content = await canonicalSkillContent(relativePath);
const original = await readOptionalSkillState(destination, resources);
if (original?.bytes.equals(Buffer.from(content, "utf8")) !== true)
return true;
let original: SkillFileState | undefined;
try {
original = await readOptionalSkillState(destination, resources);
} catch (cause: unknown) {
if (
cause instanceof RegularFileCleanupFailure ||
cause instanceof ArtifactReaderFailure
)
throw cause;
rootIssues.push({
path: destination,
kind: "unreadable",
detail: causeReason(cause),
});
if (failureCause === undefined) failureCause = cause;
rootCanonical = false;
rootHasFiles = true;
continue;
}
if (original !== undefined) rootHasFiles = true;
if (relativePath === "SKILL.md" && manifest === undefined)
manifest = original?.bytes.toString("utf8");
// A mismatch settles freshness, but not coverage of later read failures.
if (original === undefined) rootCanonical = false;
else if (rootCanonical) {
const content = await canonicalSkillContent(relativePath);
rootCanonical = original.bytes.equals(Buffer.from(content, "utf8"));
}
}
if (clientIds !== undefined || rootHasFiles || rootIssues.length > 0) {
installedDestination = true;
canonical = canonical && rootCanonical;
issues.push(...rootIssues);
}
}
return false;
return issues.length === 0
? ok({ canonical, manifest, installedDestination })
: err({
_tag: "SkillInspectionFailure",
issues,
cause: failureCause,
manifest,
});
};
/** Report whether setup would change any file in the managed REA skill bundle. */
@@ -296,20 +469,21 @@ export const canonicalSkillNeedsInstall = async (
resources?: ArtifactResourceScope,
): Promise<boolean> => {
try {
return await canonicalSkillNeedsInstallByReading(
const inspection = await inspectCanonicalSkill(
home,
clientIds,
environment,
platform,
resources,
);
return !inspection.ok || !inspection.value.canonical;
} catch (cause: unknown) {
if (
cause instanceof RegularFileCleanupFailure ||
cause instanceof ArtifactReaderFailure
)
throw cause;
// Unreadable skill state fails open to install so setup can repair it.
// A copy needs attention; selected-root admission decides whether it may be installed.
void cause;
return true;
}
@@ -431,6 +605,8 @@ export const installCanonicalSkill = async (
}
return ok("installed");
} catch (cause: unknown) {
if (stage === "prepare" && cause instanceof SetupSkillInstallError)
return err(cause);
failingPath = causePath(cause) ?? failingPath;
const rollbackFailures = await restoreSkillFiles(attempted, operations);
return err(
@@ -453,53 +629,35 @@ export const installCanonicalSkill = async (
/** Read installed skill metadata and compare every managed location. */
export const readInstalledSkillIdentity = async (
home: string,
clientIds: readonly string[],
clientIds: readonly string[] | undefined,
environment: Readonly<NodeJS.ProcessEnv> = {},
platform: NodeJS.Platform = process.platform,
resources?: ArtifactResourceScope,
): Promise<InstalledSkillIdentity | undefined> => {
const destinations = skillDestinations(
const inspection = await inspectCanonicalSkill(
home,
clientIds,
environment,
platform,
resources,
);
let content: string | undefined;
for (const destination of destinations) {
try {
content = await readSkillFile(
join(destination.path, "SKILL.md"),
resources,
);
break;
} catch (cause: unknown) {
if (
cause instanceof RegularFileCleanupFailure ||
cause instanceof ArtifactReaderFailure
)
throw cause;
// Another selected copy may provide metadata; canonical validation below
// still marks missing or unreadable copies as stale.
}
}
if (content === undefined) return undefined;
const toolCount = /^\s{2}tool_count:\s*(\d+)\s*$/mu.exec(content)?.[1];
let canonical: boolean;
try {
canonical = !(await canonicalSkillNeedsInstall(
home,
clientIds,
environment,
platform,
resources,
));
} catch (cause: unknown) {
if (!(cause instanceof ArtifactReaderFailure)) throw cause;
canonical = false;
}
if (inspection.ok && !inspection.value.installedDestination) return undefined;
const content = inspection.ok
? inspection.value.manifest
: inspection.error.manifest;
const issues = inspection.ok ? [] : inspection.error.issues;
if (content === undefined && issues.length === 0) return undefined;
const toolCount =
content === undefined
? undefined
: /^\s{2}tool_count:\s*(\d+)\s*$/mu.exec(content)?.[1];
return {
canonical,
version: /^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(content)?.[1] ?? null,
canonical: inspection.ok && inspection.value.canonical,
version:
content === undefined
? null
: (/^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(content)?.[1] ?? null),
toolCount: toolCount === undefined ? null : Number.parseInt(toolCount, 10),
...(issues.length === 0 ? {} : { issues }),
};
};
+16 -3
View File
@@ -1,6 +1,11 @@
import type { DoctorReport, DoctorScope, runDoctor } from "./Doctor.js";
import type { LinuxDistribution } from "./LinuxHopper.js";
import type { SkillDestination, SkillInstallResult } from "./SetupSkill.js";
import type {
SkillDestination,
SkillInstallResult,
SetupSkillInstallError,
} from "./SetupSkill.js";
import type { Result } from "../domain/result.js";
import type { SetupClient } from "./SupportedClients.js";
import type {
SetupFailureCode,
@@ -62,7 +67,7 @@ export interface SetupHost {
readonly homeDirectory: string;
skillDestinations(clientIds: readonly string[]): readonly SkillDestination[];
readonly registrationCommand: readonly string[];
/** Why no client may be registered with `registrationCommand` from this host. */
/** Why client registration cannot proceed from this host. */
readonly registrationCommandBlocker?: string;
readonly nodeVersion: string;
macosVersion(): Promise<string | undefined>;
@@ -89,6 +94,9 @@ export interface SetupHost {
command: readonly string[],
): Promise<ClientConfigurationInspection>;
skillNeedsInstall(clientIds: readonly string[]): Promise<boolean>;
validateSkillDirectories(
clientIds: readonly string[],
): Promise<Result<void, SetupSkillInstallError>>;
installSkill(clientIds: readonly string[]): Promise<SkillInstallResult>;
doctor(scope?: DoctorScope): Promise<Awaited<ReturnType<typeof runDoctor>>>;
}
@@ -149,7 +157,12 @@ export interface SetupClientState {
readonly client: SetupClient;
readonly detected: boolean;
readonly configured: boolean;
readonly status: "configured" | "missing" | "needs_configuration" | "invalid";
readonly status:
| "configured"
| "missing"
| "needs_configuration"
| "invalid"
| "unknown";
}
/** One concrete setup mutation disclosed before approval. */
+12 -2
View File
@@ -23,6 +23,7 @@ import {
skillDestinations,
} from "./SetupSkill.js";
import { isOwnedClientRegistrationCommand } from "./ClientRegistrationIdentity.js";
import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js";
import { dshDocument } from "./DshCordisPatch.js";
import { resolveClientConfigTransactionPath } from "./ClientConfigPath.js";
import {
@@ -562,12 +563,15 @@ const isOwnedRegistration = (value: unknown, client: SetupClient): boolean => {
.safeParse(value);
if (!parsed.success) return false;
const command = parsed.data.command;
return isOwnedClientRegistrationCommand(command);
return isOwnedClientRegistrationCommand(command, CURRENT_ENTRYPOINT_PATH);
}
const parsed = registrationSchema.safeParse(value);
if (!parsed.success) return false;
const { command, args } = parsed.data;
return isOwnedClientRegistrationCommand([command, ...args]);
return isOwnedClientRegistrationCommand(
[command, ...args],
CURRENT_ENTRYPOINT_PATH,
);
};
const purgeManagedData = async (
@@ -768,6 +772,12 @@ const removeManagedSkill = async (
const failed = results.find(({ status }) => status === "failed");
if (failed !== undefined)
return item(name, "failed", results.map(({ detail }) => detail).join(" "));
if (results.some(({ status }) => status === "retained"))
return item(
name,
"retained",
[...new Set(results.map(({ detail }) => detail))].join(" "),
);
const removed = results.some(({ status }) => status === "removed");
return item(
name,
@@ -137,6 +137,7 @@ class FakeDevice implements Device {
readonly process = { pid: 512, name: "fixture" } satisfies Process;
readonly session = new FakeSession();
resumed: number[] = [];
attachedTargets: Array<number | string> = [];
resumeFailureCount = 0;
readonly spawned: string[] = [];
@@ -161,7 +162,8 @@ class FakeDevice implements Device {
this.resumed.push(pid);
}
async attach(): Promise<Session> {
async attach(target: number | string): Promise<Session> {
this.attachedTargets.push(target);
return this.session;
}
}
@@ -207,6 +209,110 @@ const startLocalAttachedSession = async (
// eslint-disable-next-line max-lines-per-function -- related manager lifecycle cases share the same focused fake provider.
describe("FridaInstrumentationManager", () => {
it("attaches by PID when Frida omits the target from process lookup", async () => {
const bindings = new FakeDeviceManager();
bindings.device.getProcessByPid = async () => {
throw new Error("Process not found");
};
const manager = new FridaInstrumentationManager({
deviceManager: bindings,
});
const started = await manager.startSession({
mode: "attach",
deviceId: "local",
pid: 512,
});
expect(started.ok).toBe(true);
if (!started.ok) return;
expect(started.value.pid).toBe(512);
expect(bindings.device.attachedTargets).toEqual([512]);
expect(started.value.target).toBe("pid 512");
expect(bindings.device.session.isDetached()).toBe(false);
await manager.closeAll();
expect(bindings.device.session.isDetached()).toBe(true);
});
it("keeps the caller PID when Frida resolves a different process", async () => {
const bindings = new FakeDeviceManager();
bindings.device.getProcessByPid = async () => ({
...bindings.device.process,
pid: 513,
});
const manager = new FridaInstrumentationManager({
deviceManager: bindings,
});
const started = await manager.startSession({
mode: "attach",
deviceId: "local",
pid: 512,
});
expect(started.ok).toBe(true);
if (!started.ok) return;
expect(started.value.pid).toBe(512);
expect(started.value.target).toBe("fixture (pid 512)");
expect(bindings.device.attachedTargets).toEqual([512]);
await manager.closeSession(started.value.sessionId);
});
it("records a session already detached during process metadata lookup", async () => {
const bindings = new FakeDeviceManager();
let lookupStarted!: () => void;
const lookupEntered = new Promise<void>((resolve) => {
lookupStarted = resolve;
});
let finishLookup!: () => void;
const lookupGate = new Promise<void>((resolve) => {
finishLookup = resolve;
});
bindings.device.getProcessByPid = async () => {
lookupStarted();
await lookupGate;
return bindings.device.process;
};
const manager = new FridaInstrumentationManager({
deviceManager: bindings,
});
const starting = manager.startSession({
mode: "attach",
deviceId: "local",
pid: 512,
});
await lookupEntered;
bindings.device.session.markDetached();
finishLookup();
const started = await starting;
expect(started.ok).toBe(true);
if (!started.ok) return;
expect(started.value.pid).toBe(512);
expect(bindings.device.attachedTargets).toEqual([512]);
expect(started.value.state).toBe("detached");
expect(manager.status(started.value.sessionId)?.state).toBe("detached");
await manager.closeSession(started.value.sessionId);
});
it("still reports a real attach failure when process lookup is unavailable", async () => {
const bindings = new FakeDeviceManager();
bindings.device.getProcessByPid = async () => {
throw new Error("Process not found");
};
bindings.device.attach = async () => {
throw new Error("attach denied");
};
const manager = new FridaInstrumentationManager({
deviceManager: bindings,
});
const started = await manager.startSession({
mode: "attach",
deviceId: "local",
pid: 512,
});
expect(started.ok).toBe(false);
if (started.ok) return;
expect(JSON.stringify(started.error)).toContain("attach denied");
await manager.closeAll();
});
it("retains a failed startup's spawn and remote lease until cleanup succeeds", async () => {
const bindings = new FakeDeviceManager();
bindings.device.attach = async () => {
@@ -28,6 +28,7 @@ it("retains batched TraceMap-resolved paths and null source positions", () => {
text: { included: true, value: sourceMapText },
},
],
unavailable_relevant_files: [],
containers: [],
text_bytes_read: Buffer.byteLength(sourceMapText),
invalid_utf8_files: 0,
@@ -178,6 +178,7 @@ const graphCoverage = (context: JavaScriptArtifactGraphContext) => {
);
const unknownGap =
(context.fileSet.source_selection?.unselected_files ?? 0) > 0 ||
context.fileSet.unavailable_relevant_files.length > 0 ||
context.analysis.files.some(
({ analysis_failure, application_projection_failure }) =>
analysis_failure !== undefined ||
@@ -1,7 +1,10 @@
import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
import { htmlInlineScriptLimitation } from "./JavaScriptArtifactAnalysis.js";
import { completeApplicationCoverage } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
import {
completeApplicationCoverage,
partialApplicationCoverage,
} from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import { freezeJsonSnapshot } from "../../domain/immutableJson.js";
@@ -300,8 +303,133 @@ export const addJavaScriptArtifactFiles = (
},
});
}
addUnavailableAsarEntries(context);
};
const addUnavailableAsarEntries = (
context: JavaScriptArtifactGraphContext,
): void => {
for (const file of context.fileSet.unavailable_relevant_files) {
if (!isAsarContainer(context, file.container_sha256)) continue;
const limitations = [
...(file.limitations ?? []),
`The inventoried ${file.kind ?? "relevant"} entry has unavailable bytes; no content artifact identity can be established.`,
];
const entry = context.accumulator.addNode({
kind: "asar-entry",
identity: {
strategy: "canonical-path",
stability: "artifact-version",
artifact_sha256: file.container_sha256,
path: file.path,
},
observations: [
{
label: file.path,
properties: {
entry_sha256: file.sha256,
bytes: file.bytes,
inventory_artifact_id: file.inventory_artifact_id,
...(file.inventory_occurrence_id === undefined
? {}
: { inventory_occurrence_id: file.inventory_occurrence_id }),
hash_status: file.hash_status,
artifact_kind: file.artifact_kind,
...(file.kind === undefined ? {} : { kind: file.kind }),
content_available: false,
},
evidence: artifactObservationEvidence({
sha256: file.container_sha256,
path: file.path,
operation: "inventory-asar-unavailable-entry",
coverage: partialApplicationCoverage([], null),
limitations,
}),
},
],
});
const parent =
context.containerNodes.get(file.container_sha256) ?? context.root;
context.accumulator.addEdge({
source_node_id: parent.node_id,
target_node_id: entry.node_id,
relation: "contains",
properties: {
path: file.path,
content_available: false,
hash_status: file.hash_status,
},
evidence: artifactObservationEvidence({
sha256: file.container_sha256,
path: file.path,
operation: "inventory-asar-unavailable-entry",
coverage: partialApplicationCoverage([], null),
limitations,
}),
});
if (file.kind === "native-addon" || file.artifact_kind === "native-addon") {
const addon = context.accumulator.addNode({
kind: "native-addon",
identity: {
strategy: "canonical-path",
stability: "artifact-version",
artifact_sha256: file.container_sha256,
path: file.path,
},
observations: [
{
label: file.path,
properties: {
availability: "unavailable",
...(file.hash_status === undefined
? {}
: { hash_status: file.hash_status }),
...(file.artifact_kind === undefined
? {}
: { artifact_kind: file.artifact_kind }),
bytes: file.bytes,
inventory_artifact_id: file.inventory_artifact_id,
...(file.inventory_occurrence_id === undefined
? {}
: { inventory_occurrence_id: file.inventory_occurrence_id }),
},
evidence: artifactObservationEvidence({
sha256: file.container_sha256,
path: file.path,
operation: "classify-unavailable-native-addon",
coverage: partialApplicationCoverage([], null),
limitations,
}),
},
],
});
context.accumulator.addEdge({
source_node_id: entry.node_id,
target_node_id: addon.node_id,
relation: "contains",
properties: {
classification: "native-addon",
content_available: false,
},
evidence: artifactObservationEvidence({
sha256: file.container_sha256,
path: file.path,
operation: "classify-unavailable-native-addon",
coverage: partialApplicationCoverage([], null),
limitations,
}),
});
}
}
};
const isAsarContainer = (
context: JavaScriptArtifactGraphContext,
containerSha256: string,
): boolean =>
context.snapshot.manifest.root_format === "asar" ||
containerSha256 !== context.snapshot.manifest.root_sha256;
const sourceFileRange = (
file: JavaScriptArtifactFile,
): JavaScriptSourceRange => {
@@ -464,10 +592,7 @@ const createAsarEntry = (
context: JavaScriptArtifactGraphContext,
file: JavaScriptArtifactFile,
): ApplicationNode | undefined => {
const isAsar =
context.snapshot.manifest.root_format === "asar" ||
file.container_sha256 !== context.snapshot.manifest.root_sha256;
if (!isAsar) return undefined;
if (!isAsarContainer(context, file.container_sha256)) return undefined;
return context.accumulator.addNode({
kind: "asar-entry",
identity: {
@@ -68,6 +68,56 @@ describe("artifact-local graph identity", () => {
});
describe("loader-owned path lookup", () => {
it("applies URL whitespace handling only to ESM lookup and retains declarations", () => {
const tab = "\t";
const files = fileMap([
file("app/entry.mjs", "root"),
file("app/feature.js", "root"),
file(`app/tab${tab}name.js`, "root"),
]);
for (const whitespace of ["\t", "\n", "\r"]) {
const raw = `./fea${whitespace}ture.js`;
expect(
resolve({
declaredPath: raw,
sourcePath: "app/entry.mjs",
context: "module-specifier",
moduleKind: "import",
files,
}),
).toMatchObject({ declared_path: raw, resolved_path: "app/feature.js" });
}
expect(
resolve({
declaredPath: `./tab${tab}name.js`,
sourcePath: "app/entry.mjs",
context: "module-specifier",
moduleKind: "import",
files,
}),
).toMatchObject({ resolved_path: null });
expect(
resolve({
declaredPath: `./tab${tab}name.js`,
sourcePath: "app/entry.cjs",
context: "module-specifier",
moduleKind: "require",
files,
}),
).toMatchObject({ resolved_path: "app/tab\tname.js" });
expect(
resolve({
declaredPath: "./tab%09name.js",
sourcePath: "app/entry.mjs",
context: "module-specifier",
moduleKind: "import",
files,
}),
).toMatchObject({ resolved_path: "app/tab\tname.js" });
});
});
describe("loader-specific module suffix lookup", () => {
it("keeps relative ESM imports exact", () => {
const files = fileMap([
file("app/entry.mjs", "root"),
@@ -186,6 +236,183 @@ describe("loader-owned path lookup", () => {
});
});
describe("unavailable inventoried native add-ons", () => {
it("retains an inventoried native add-on whose bytes are unavailable", () => {
const unavailable = {
path: "app/addon.node",
container_sha256: file("app/entry.cjs", "root").container_sha256,
sha256: null,
bytes: 8192,
inventory_artifact_id: null,
inventory_occurrence_id: "missing-addon-occurrence",
kind: "native-addon" as const,
hash_status: "unavailable" as const,
limitations: ["unpacked companion is missing"],
};
const result = resolve({
declaredPath: "./addon.node",
sourcePath: "app/entry.cjs",
context: "module-specifier",
moduleKind: "require",
files: fileMap([file("app/entry.cjs", "root")]),
unanalyzedFiles: {
get: (path) => (path === unavailable.path ? unavailable : undefined),
hasDirectory: () => false,
},
});
expect(result).toMatchObject({
declared_path: "./addon.node",
resolution_status: "unavailable",
candidate_path: "app/addon.node",
limitations: [
"unpacked companion is missing",
expect.stringContaining("inventoried native-addon file"),
],
});
});
});
describe("unavailable package metadata", () => {
it("blocks directory fallback until same-container manifests are readable", () => {
const manifest = {
path: "app/relative/package.json",
container_sha256: file("app/main.cjs", "root").container_sha256,
sha256: null,
bytes: 28,
inventory_artifact_id: null,
inventory_occurrence_id: "missing-relative-manifest",
kind: "package-json" as const,
hash_status: "unavailable" as const,
limitations: ["unpacked manifest companion is missing"],
};
const files = fileMap([
file("app/main.cjs", "root"),
file("app/main.mjs", "root"),
file("app/relative/index.js", "root"),
]);
const requestedPaths: string[] = [];
const lookup = {
get: (path: string) => {
requestedPaths.push(path);
return path === manifest.path ? manifest : undefined;
},
hasDirectory: () => false,
};
const relativeRequire = resolve({
declaredPath: "./relative",
sourcePath: "app/main.cjs",
context: "module-specifier",
moduleKind: "require",
files,
unanalyzedFiles: lookup,
});
expect(requestedPaths).toContain(manifest.path);
expect(relativeRequire).toMatchObject({
resolution_status: "unavailable",
resolved_path: null,
limitations: ["unpacked manifest companion is missing"],
});
expect(
resolve({
declaredPath: "./relative",
sourcePath: "app/main.mjs",
context: "module-specifier",
moduleKind: "import",
files,
unanalyzedFiles: lookup,
}),
).toMatchObject({ resolution_status: "not-found", resolved_path: null });
const restoredFiles = fileMap([
...files.values(),
file("app/relative/package.json", "root", '{"main":"public.js"}'),
file("app/relative/public.js", "root"),
]);
expect(
resolve({
declaredPath: "./relative",
sourcePath: "app/main.cjs",
context: "module-specifier",
moduleKind: "require",
files: restoredFiles,
unanalyzedFiles: lookup,
}),
).toMatchObject({
resolution_status: "resolved",
resolved_path: "app/relative/public.js",
});
});
it("keeps bare package exports unknown while its manifest is unavailable", () => {
const containerSha256 = file("app/main.mjs", "root").container_sha256;
const manifest = {
path: "app/node_modules/fixture/package.json",
container_sha256: containerSha256,
sha256: null,
bytes: 47,
inventory_artifact_id: null,
kind: "package-json" as const,
hash_status: "unavailable" as const,
limitations: ["unpacked exports manifest is missing"],
};
const files = fileMap([
file("app/main.cjs", "root"),
file("app/main.mjs", "root"),
file("app/node_modules/fixture/index.js", "root"),
file("app/node_modules/fixture/import.js", "root"),
file("app/node_modules/fixture/require.js", "root"),
]);
const lookup = {
get: (path: string) => (path === manifest.path ? manifest : undefined),
hasDirectory: () => false,
};
for (const [sourcePath, moduleKind] of [
["app/main.cjs", "require"],
["app/main.mjs", "import"],
] as const)
expect(
resolve({
declaredPath: "fixture",
sourcePath,
context: "module-specifier",
moduleKind,
files,
unanalyzedFiles: lookup,
}),
).toMatchObject({
resolution_status: "unavailable",
resolved_path: null,
limitations: ["unpacked exports manifest is missing"],
});
const restoredFiles = fileMap([
...files.values(),
file(
manifest.path,
"root",
'{"exports":{"import":"./import.js","require":"./require.js"}}',
),
]);
for (const [sourcePath, moduleKind, target] of [
["app/main.cjs", "require", "app/node_modules/fixture/require.js"],
["app/main.mjs", "import", "app/node_modules/fixture/import.js"],
] as const)
expect(
resolve({
declaredPath: "fixture",
sourcePath,
context: "module-specifier",
moduleKind,
files: restoredFiles,
unanalyzedFiles: lookup,
}),
).toMatchObject({
resolution_status: "resolved",
resolved_path: target,
});
});
});
describe("bare package root file lookup", () => {
it("uses loader-specific bare package roots when only a sibling .js file exists", () => {
const absentDirectory = fileMap([
@@ -499,6 +499,7 @@ const contextualCandidate = (
}
const hasUrlPathSemantics =
context === "module-specifier" && input.moduleKind === "import";
if (hasUrlPathSemantics) declared = htmlUrlText(declared);
const rejected = rejectDeclaration(
declared,
declaresLiteralPercent(input) || hasUrlPathSemantics,
@@ -1143,10 +1144,21 @@ const readPackageMetadata = (
const path = posix.join(directory, "package.json");
const file = input.files.get(path);
const source = input.files.get(input.sourcePath);
if (
file === undefined ||
(source !== undefined && file.container_sha256 !== source.container_sha256)
)
if (file === undefined) {
const unavailable = input.unanalyzedFiles?.get(path);
if (
source !== undefined &&
unavailable?.container_sha256 === source.container_sha256
)
return {
kind: "unavailable",
limitation:
unavailable.limitations?.[0] ??
`Directory package metadata ${path} was inventoried but its bytes are unavailable.`,
};
return { kind: "absent" };
}
if (source !== undefined && file.container_sha256 !== source.container_sha256)
return { kind: "absent" };
if (!file.text.included)
return {
@@ -1400,7 +1412,9 @@ const findFileCandidate = (
candidatePath: path,
limitations: [
...(unanalyzed.limitations ?? []),
`${path} exists in the inventoried artifact container (sha256 ${unanalyzed.sha256 ?? "unknown"}, ${unanalyzed.bytes === null ? "unknown size" : `${String(unanalyzed.bytes)} bytes`}) but is not a JavaScript, JSON, HTML, source map, or native add-on file; its contents were not analyzed.`,
unanalyzed.kind === undefined
? `${path} exists in the inventoried artifact container (sha256 ${unanalyzed.sha256 ?? "unknown"}, ${unanalyzed.bytes === null ? "unknown size" : `${String(unanalyzed.bytes)} bytes`}) but is not a JavaScript, JSON, HTML, source map, or native add-on file; its contents were not analyzed.`
: `${path} is an inventoried ${unanalyzed.kind} file, but its bytes are unavailable (hash status ${unanalyzed.hash_status ?? "unknown"}); resolution cannot be confirmed.`,
],
};
return null;
@@ -261,7 +261,8 @@ export const reconstructJavaScriptArtifact = async (
),
),
statistics: {
relevant_files: files.files.length,
relevant_files:
files.files.length + files.unavailable_relevant_files.length,
nested_asar_containers: files.containers.length,
text_bytes_read: files.text_bytes_read,
invalid_utf8_files: files.invalid_utf8_files,
@@ -25,6 +25,43 @@ import { semanticCoverageResourceLimits } from "../../domain/javascript/javascri
const SHA256 = "a".repeat(64);
it.each([
["plain export", "const answer = 42; export { answer };", 0],
["aliased export", "const answer = 42; export { answer as result };", 0],
["declaration only", "const answer = 42;", 0],
["genuine assignment", "let answer = 0; answer = 1;", 1],
])(
"projects writes only for actual writes: %s",
(_name, source, writeCount) => {
const graph = graphFor(source);
const binding = graph.nodes.find(
({ kind, label }) => kind === "binding" && label === "answer",
);
if (binding === undefined) throw new Error("Expected answer binding");
const writes = graph.relations.filter(
({ relation, target_node_id }) =>
relation === "writes" && target_node_id === binding.node_id,
);
expect(writes).toHaveLength(writeCount);
const exportReference = graph.nodes.find(
({ identity }) => identity.role_key === "reference:0:export:answer",
);
if (source.includes("export {")) expect(exportReference).toBeDefined();
const trace = queryJavaScriptSemanticGraph(graph, {
seed: { kind: "semantic-node", node_id: binding.node_id },
direction: "backward-provenance",
allowed_relations: ["writes"],
});
expect(trace.status).toBe("found");
expect(trace.summary).toMatchObject({
total_seed_matches: 1,
traversed_relations: writeCount,
});
expect(trace.relations).toHaveLength(writeCount);
},
);
it("projects closure and direct interprocedural flow without execution", () => {
const graph = graphFor(`
const outer = 2;
@@ -648,7 +648,7 @@ function* projectDefinitionsAndReferences(
resolution: "resolved",
evidence: inferredSemanticEvidenceAt(context.file, reference.location),
});
else
else if (reference.role === "write")
addRelation(context.state, {
source: expression,
target: binding,
+10
View File
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import {
ArtifactPathRegistry,
annotateEnglishUnicodeCaseCollisions,
ENGLISH_UNICODE_CASE_INVENTORY_LIMITATION,
} from "./ArtifactPaths.js";
@@ -26,6 +27,15 @@ const annotate = (paths: readonly string[]) => {
};
describe("English Unicode case collisions", () => {
it("retains repeated same-kind entries while rejecting file-directory aliasing", () => {
const registry = new ArtifactPathRegistry();
registry.add("duplicate", "file");
expect(() => registry.add("duplicate", "file")).not.toThrow();
expect(() => registry.add("duplicate", "directory")).toThrow(
"Artifact path collision",
);
});
it("names the other spelling of a colliding file", () => {
const { limitation, byPath } = annotate([".", "Main.js", "main.js", "x"]);
expect(limitation).toBe(ENGLISH_UNICODE_CASE_INVENTORY_LIMITATION);
+1 -1
View File
@@ -61,7 +61,7 @@ export class ArtifactPathRegistry {
}
node = child;
if (index === parts.length - 1) {
if (node.kind !== undefined)
if (node.kind !== undefined && node.kind !== kind)
throw new ArtifactReaderFailure(
"path",
`Artifact path collision: ${path}`,
Loaded 100 of 359 files, more files were not shown because too many files have changed in this diff. Show more