Compare commits

...
Author SHA1 Message Date
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
b33236ec08 chore(release/5.0.0): release rea-agents 5.0.0 (#1007)
Release / Release Please (push) Canceled after 0s
Release / Publish to npm (push) Canceled after 0s
Release / Publish to MCP Registry (push) Canceled after 0s
* chore(release/5.0.0): release rea-agents 5.0.0

* chore: normalize release product catalog

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-08 01:26:36 +08:00
N0zoM1z0 fc58fa1eb0 fix(release): drive checkpoint releases from frozen branches
Use branch pushes for Release Please preparation and reviewed-merge publication.
Reject moved source refs before tagging and bind registry provenance to the
triggering release commit. Remove phase inputs and document the three steps.

Assisted-by: Sakuya (gensokyo-skills:sakuya-checkpointed-execution)
Assisted-by: Cirno (gensokyo-skills:cirno-radical-simplification)
2026-10-07 17:02:03 +00:00
N0zoM1z0 49887f0239 fix(release): publish from explicit source checkpoints
Dispatch preparation and publication against a frozen release branch.
Bind package builds and provenance to the same tagged commit, enable
release-branch PR CI, and document the maintainer workflow.

Assisted-by: Sakuya (gensokyo-skills:sakuya-checkpointed-execution)
2026-10-07 16:38:02 +00:00
morlutoandGitHub 4a6073810a fix(contracts): align CLI/MCP boundary contracts (#946)
* fix(cli): validate native scenario and preserve JSON paths

* fix(process): reject invalid UTF-8 JSON inputs

* fix(cli): retain missing JSON path diagnostics

* test(cli): cover relative brace and bracket JSON paths

* test(cli): reject missing native scenario steps

* fix(mcp): advertise reserved process environment key

* fix(process): reject NUL in process scenario values

* fix(process): advertise NUL input constraints to MCP

* fix(cli): report scenario input schema issues

* fix(contracts): align CLI and MCP input validation

* fix(mcp): document semantic output constraints

* fix(mcp): describe process journal compatibility

* docs: regenerate catalog after schema grouping

* docs: regenerate catalog after main merge

* fix(cli): preserve JSON and native UI input diagnostics

* test(mcp): reuse advertised tool schemas in managed flow
2026-10-08 00:03:36 +08:00
morlutoandGitHub fc4a9a0592 fix(artifacts): preserve reader failure diagnostics (#987)
* fix(artifacts): preserve filesystem and DMG diagnostics

* fix(artifacts): classify DMG verification failures

* fix(artifacts): distinguish inaccessible disk images

* fix(artifacts): classify damaged image checksums
2026-10-07 23:58:07 +08:00
morlutoandGitHub abd7a5ed09 perf(artifacts): stream ASAR members without full buffering (#962)
* fix(artifacts): stream ASAR members without full buffering

* fix(artifacts): close unopened ASAR member streams

* fix(artifacts): preserve unpacked ASAR mismatch digests

* fix(artifacts): preserve ASAR read failures

* fix(artifacts): retain unpacked ASAR stat failures

* fix(artifacts): validate ASAR entry producer identity
2026-10-07 23:57:05 +08:00
morlutoandGitHub 58381eefa2 perf(inspector): bound runtime script location authorization (#945)
* perf(inspector): authorize script locations concurrently

* fix(inspector): bound concurrent location authorization

* fix(inspector): stop queued authorization on failure

* test(inspector): avoid freezing authorization scheduling
2026-10-07 23:54:31 +08:00
morlutoandGitHub ebb065e2dc perf(comparison): reuse canonical sort keys (#990) 2026-10-07 23:54:09 +08:00
morlutoandGitHub 2cedde9f1c perf(javascript): index Promise return-site ownership (#971)
* perf(javascript): index promise return ownership lookups

* perf(javascript): defer Promise return-site indexing
2026-10-07 23:53:41 +08:00
morlutoandGitHub 248972f327 fix(installer): validate exact semantic versions (#954)
* fix(installer): validate exact semantic versions

* fix(installer): reject npm-ambiguous build metadata
2026-10-07 23:53:26 +08:00
YsopwnableandGitHub 5047373006 feat(website): add a shared back-to-top link
Long guides and case studies now have a small `↑ Top` link at the bottom right. It appears after scrolling, returns smoothly to the page header, and moves keyboard focus back to navigation. Reduced-motion users return immediately; the native `#top` link also works without JavaScript.

The link is shared by all 11 styled website pages. The README and style guide document it, and the evidence notes retain the original browser-example digest alongside its later navigation addition.

Validation:

- Website preparation and verification: 12 HTML pages, 195 local references, SVG XML, the six-file ZIP and the sole manual Pages publisher.
- Oxfmt, `git diff --check` and `npm run check:fast` with Node.js 24.18.0.
- Focused browser checks at `/` and `/rea/`: 22 routes, 21 pointer returns, six keyboard returns, five viewport widths, smooth scrolling, reduced motion, focus return and the no-JavaScript anchor.
- Existing site interactions: 24 routes, 68 copy buttons, 20 comparison steps, two CSV exports, two ZIP downloads and eight viewport widths from 320px to 1440px. Code excerpts are unchanged.
- Desktop and mobile rendering reviewed. Downloadable Electron source and CSV export source/data are unchanged.

Publication continues through the existing manual Pages workflow after merge.
2026-10-07 23:50:31 +08:00
morlutoandGitHub 7dc40d3d9e perf(evidence): scale imports and dependency graphs (#941)
* perf(evidence): avoid rebuilding ledger on record imports

* perf(evidence): scale unknown graph validation

* fix(evidence): preserve import and projection semantics

* fix(evidence): invalidate cache on metadata replacement

* test(evidence): isolate snapshot import regression

* fix(evidence): preserve snapshots on additive imports
2026-10-07 23:47:47 +08:00
morlutoandGitHub 93aa9eb5c2 fix(process): harden filesystem snapshot identity and cancellation (#956)
* fix(process): bind snapshots to captured file identity

* fix(process): honor snapshot cancellation for empty files

* test(process): verify snapshot descriptor cleanup

* fix(process): preserve snapshot cancellation results

* fix(process): preserve non-cancellation capture failures

* test(process): verify descriptor cleanup with fstat

* fix(process): detect snapshot changes by ctime

* test(process): prove snapshot ctime invalidation
2026-10-07 23:45:23 +08:00
morlutoandGitHub d76f7d1508 fix(windows): remove cancelled snapshot outputs (#957)
* fix(windows): remove cancelled snapshot outputs

* fix(windows): allow readback of retained runtime files

* test(windows): verify retained runtime file protection
2026-10-07 23:45:16 +08:00
morlutoandGitHub 9f496ef2d4 fix(native)!: preserve incomplete native UI captures (#991)
* fix(native): preserve unknown accessibility child counts

* fix(native): align helper buffer with capture budget

* fix(native): align Swift helper JSON wire size

* fix(native): bound raw helper output to scenario budget
2026-10-07 23:44:41 +08:00
YsopwnableandGitHub 5065fcd520 docs(website): clarify guides and case studies
Several guides use broad headings and repeat their summaries, making the main example harder to follow. This change names each task directly, keeps Notion's clipboard investigation in the main reading path, and moves the Markdown example and longer provenance into expandable details. The browser guide now opens the hosted example, and the first native CLI query works without a global install.

The existing figures, prompts, source excerpts and comparison controls remain the teaching material. Provider details now fit narrow screens. The repository README links to all Showcases, and the website authoring docs share a signed design principle: a good website is like a good paper—easy to follow, clear and concise, with a clean, refined presentation. A new style guide and scoped AGENTS.md explain how to maintain that approach.

Closes #985.

Validation:

- `python3 scripts/prepare-website.py` and `python3 scripts/verify-website.py`: 12 HTML pages, 183 local references, SVG XML, the six-file ZIP and the sole manual Pages publisher.
- Oxfmt checks and `git diff --check`; `npm run check:fast` with Node.js 24.18.0.
- Browser checks at both `/` and `/rea/`: 24 routes, 68 copy buttons, 20 comparison steps, two CSV exports, two ZIP downloads and eight viewport widths from 320px to 1440px.
- Existing code excerpts, expanded details, keyboard access, no-JavaScript content, continuous cursor animation and reduced motion checked.

Existing analysis and compiler-check evidence is retained; real native-provider verification and compiler replay were not rerun for these copy and layout changes. The homepage content and publication triggers are unchanged.
2026-10-07 23:24:15 +08:00
morlutoandGitHub 0a4caf4a0a fix(evaluation): canonicalize Unicode argument keys (#980) 2026-10-07 23:19:49 +08:00
98b2b027af fix(linux): improve Hopper reliability and CachyOS support (#963)
* fix(linux): support CachyOS Hopper hosts

* fix(config): reuse user-local Hopper launcher

* fix(hopper): harden Linux demo dialog handling

* chore: retrigger CI after PR title fix

* fix(linux): align Hopper defaults and support docs

---------

Co-authored-by: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com>
2026-10-07 23:16:48 +08:00
morlutoandGitHub 8aaf9a1978 fix(firmware): retain multi-file extraction failures (#978) 2026-10-07 23:06:41 +08:00
morlutoandGitHub d5c43d65f0 fix(cli): preserve output and comparison input validation (#981) 2026-10-07 23:04:54 +08:00
morlutoandGitHub a45fe86a39 fix(build): validate installed dependencies before cache hits (#982) 2026-10-07 23:01:01 +08:00
morlutoandGitHub 71ac83f3dd fix(apple): traverse keyed archive hierarchy collections (#976) 2026-10-07 23:00:43 +08:00
morlutoandGitHub fb3749aff6 fix(android): align runtime suffix inference (#977) 2026-10-07 23:00:22 +08:00
morlutoandGitHub f26c8b5b8c perf(reconstruction): index coverage evaluation (#983) 2026-10-07 22:59:13 +08:00
YsopwnableandGitHub ea4db4c9f7 refactor(native): group analyst semantics workflows and contracts (#973) 2026-10-07 22:40:48 +08:00
YsopwnableandGitHub f8d84da127 docs(website): add TH04 bullet-ring case study
Adds a TH04 case study showing how its original 16-bit PC-98 DOS code spaces a bullet ring and aims it at the player. Fresh REA instructions are paired with a five-step C++ comparison and a ring diagram; homepage and showcase-index entries link to the new page and the requested reconstruction repository.

The page credits TH04's maintained source and recorded compiler checks separately from the new REA inspection. Public provenance pins the target, source checkpoint and evidence. No original game assets, executables, private paths or raw result bundles are included. Pages remains manually dispatched.

Validation:
- Website verifier: 12 HTML pages, 185 local references, SVG XML and downloadable ZIP.
- Browser: root and `/rea/`, 32 copy checks, five comparison steps, eight viewport widths, keyboard, reduced motion and no-JavaScript content.
- Fresh real Ghidra verification through published REA 4.1.0: DOS load image verified, all 84 checks match; shown instructions match the function dossier. No Hopper verification was needed or run.
- Independent claims/privacy reviews passed; seven immutable public source links match the local TH04 checkpoint.
- `npm run check:fast`.

The two clean compiler replays and 2,139 matching bytes are recorded TH04 checks, reviewed against their retained receipt and source hashes; the website update does not rerun the compiler or game.
2026-10-07 22:09:16 +08:00
YsopwnableandGitHub 33d4761cc7 refactor(binary): move production composition outside application workflows (#965) 2026-10-07 21:56:58 +08:00
b5afa5f91e feat(browser): attribute native execution and listener sources (#917)
* feat(browser): attribute native execution and listener sources

Integrate finite native precise-coverage windows and direct DOM listener inspection through shared CLI/MCP workflows, exact session script identities, inline source evidence and owned instrumentation cleanup.

Refs #881, #774

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)

* fix(browser): preserve runtime identity and settle fixture cleanup

Retain complete reported initiator objects, reject changing loader availability, propagate the selected operation through main-frame authorization and complete effect/help inventories. Close only the synthetic owned Chrome via its native endpoint, await process-group settlement and preserve primary verification errors during cleanup.

Refs #881

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)

* fix(browser): settle invalid events and preserve document evidence

Accept same-document URL changes with proven stable loader identity, retain metadata without inventing ownership, terminate malformed event collection immediately, and sanitize known transport URLs without erasing sourceURL declarations.

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)

* fix(browser): end detached target windows and sanitize coverage identities

Reuse the existing flat-session event matcher, retain prearming termination, and apply the source identity policy to coverage URLs. Add real MCP target-close verification and producer regressions for detachment orderings.

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)

* fix(browser): retain final sample metadata and settle fatal protocol failures

Keep source inventory open through the resetting sample, propagate fatal wire failures without outstanding commands, and check listener collection failures after final document validation.

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)

* fix(browser): require default execution worlds for website attribution

Preserve native context and world metadata without treating same-frame isolated or unknown contexts as website sources. Verify actual Chrome isolated function execution before excluding its coverage, and cover ambiguous world metadata with producer regressions.

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)

* fix(browser): revalidate runtime source joins and selector failures

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-07 21:45:07 +08:00
ysopwnableandGitHub ef1a0cc65a refactor(apple): group artifact producers and verification (#952)
* refactor(apple): group artifact producers and verification

* test(apple): run affected native plist CLI regressions
2026-10-07 21:09:33 +08:00
ysopwnableandGitHub a6ba772c08 docs(website): add Notion case study and improve onboarding
The Electron guide currently uses a teaching fixture. Add a real Notion clipboard case with REA findings, short source excerpts and responsive figures, alongside the existing DX-Ball reconstruction.

- Trace the saved Notion 7.6.1 preload API through its wrappers to the clipboard receiver. Attribute the separate web-cache module probes to their original harness. Public content uses relative source locations and generic example data; packages and complete captures stay local.
- Add a Showcases index and links from the homepage and JavaScript guide. Simplify homepage copy while preserving the approved headline and closing question.
- Show copyable agent installation prompts and the direct setup command. Keep terminal cursors blinking consistently and respect reduced-motion settings.
- Provide one Notes example ZIP. Generate it from the six public source files before website checks and manual Pages deployment; verify its contents against the source. Pages remains manual-only.

Validation:
- `npm run check:fast`
- `python3 scripts/prepare-website.py` and `python3 scripts/verify-website.py`
- Browser checks at `/` and `/rea/`: downloads, prompt/command copying, responsive layouts, keyboard navigation, reduced motion and DX-Ball comparison controls.
- Published REA 4.1.0 preload analysis and feature traces; source/claim/privacy review.
- Real Hopper/Ghidra verification: not performed (website-only change).
2026-10-07 20:59:56 +08:00
345 changed files with 14916 additions and 1157 deletions
+4 -2
View File
@@ -4,7 +4,7 @@ on:
push:
branches: [main]
pull_request:
branches: [main]
branches: [main, "release/*"]
concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
@@ -311,7 +311,9 @@ jobs:
- name: Real binary metadata and Interface Builder integration
run: npm run verify:apple-dispatch && npm run verify:interface-builder
- name: Native plist CLI selection regressions
run: npm run test:focused -- tests/boundary/cli/nativePlistCli.test.ts
run: npm run test:focused -- tests/boundary/cli/nativePlistCli.test.ts tests/boundary/cli/nativePlistPrototypeKeyCli.test.ts tests/boundary/cli/nativePlistSelectionFailureCli.test.ts
- name: Native Swift demangling CLI and MCP regressions
run: npm run test:focused -- tests/boundary/cli/nativeDemangleCli.test.ts tests/boundary/mcp/nativeDemangleMcp.test.ts
reconstruction-readiness:
name: Reconstruction readiness
+54
View File
@@ -0,0 +1,54 @@
name: Real website runtime attribution
on:
pull_request:
paths:
- ".github/workflows/real-web-runtime.yml"
- "scripts/verify-browser-runtime.mjs"
- "scripts/lib/browser-runtime-*.mjs"
- "src/process/ProviderProcess.ts"
- "src/process/ProcessOwnership*.ts"
- "scripts/fixtures/browser-runtime-site.mjs"
- "src/browser/execution/**"
- "src/browser/CdpConnection.ts"
- "src/browser/CdpEndpoint.ts"
- "src/browser/CdpTarget*.ts"
- "src/browser/CdpCaptureValues.ts"
- "src/browser/CdpAuthorized*.ts"
- "src/domain/webRuntime.ts"
- "src/domain/webExecution.ts"
- "src/domain/webEventListeners.ts"
- "src/domain/browserObservationError*.ts"
- "src/application/WebRuntime*.ts"
- "src/composition/webRuntime.ts"
- "src/contracts/webRuntimeToolContracts.ts"
- "src/contracts/toolEffects.ts"
- "src/server/registerWebRuntimeTools.ts"
- "src/server/createServer.ts"
- "src/cli/webRuntimeCommands.ts"
- "src/cli/commandCancellation.ts"
- "src/cli.ts"
- "package.json"
- "package-lock.json"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: real-web-runtime-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
HUSKY: "0"
jobs:
real-web-runtime:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run verify:browser:runtime
env:
REA_BROWSER_EXECUTABLE: /usr/bin/google-chrome
REA_BROWSER_NO_SANDBOX: "true"
+40 -2
View File
@@ -2,7 +2,8 @@ name: Release
on:
push:
branches: [main]
branches: ["release/*"]
workflow_dispatch:
env:
HUSKY: "0"
@@ -21,13 +22,47 @@ jobs:
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
sha: ${{ steps.release.outputs.sha }}
steps:
- name: Validate release selection
shell: bash
env:
GH_TOKEN: ${{ github.token }}
RELEASE_BRANCH: ${{ github.ref_name }}
run: |
if [[ "${GITHUB_REF}" != "refs/heads/${RELEASE_BRANCH}" || "${RELEASE_BRANCH}" != release/* ]]; then
echo "Select a release/ branch; main and tags are not release checkpoints." >&2
exit 1
fi
if ! git check-ref-format "refs/heads/${RELEASE_BRANCH}"; then
echo "Invalid release branch: ${RELEASE_BRANCH}" >&2
exit 1
fi
selected_sha="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/${RELEASE_BRANCH}" --jq '.object.sha')"
if [[ "${selected_sha}" != "${GITHUB_SHA}" ]]; then
echo "The release branch moved after this run was triggered; review its new checkpoint before publication." >&2
exit 1
fi
printf 'Selected release checkpoint: %s\n' "${selected_sha}"
- uses: googleapis/release-please-action@v4
id: release
with:
# A dedicated credential lets release-PR updates trigger CI. GitHub
# requires manual approval for pull_request events from GITHUB_TOKEN.
token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
target-branch: ${{ github.ref_name }}
- name: Bind publication to the triggering checkpoint
if: steps.release.outputs.release_created == 'true'
shell: bash
env:
RELEASE_SHA: ${{ steps.release.outputs.sha }}
run: |
if [[ "${RELEASE_SHA}" != "${GITHUB_SHA}" ]]; then
echo "The tagged release SHA differs from the triggering checkpoint; refusing to publish mismatched source provenance." >&2
exit 1
fi
- name: Check out release pull request
if: steps.release.outputs.prs_created == 'true'
@@ -99,6 +134,9 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.release-please.outputs.sha }}
persist-credentials: false
- uses: actions/setup-node@v6
with:
@@ -164,7 +202,7 @@ jobs:
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.release-please.outputs.tag_name }}
ref: ${{ needs.release-please.outputs.sha }}
persist-credentials: false
- name: Verify package and registry metadata versions
+3
View File
@@ -6,6 +6,7 @@ on:
paths:
- "website/**"
- "scripts/verify-website.py"
- "scripts/prepare-website.py"
- ".github/workflows/**"
workflow_dispatch:
@@ -24,5 +25,7 @@ jobs:
steps:
- name: Check out the repository
uses: actions/checkout@v4
- name: Prepare the example download
run: python3 scripts/prepare-website.py
- name: Check website links and publishing configuration
run: python3 scripts/verify-website.py
+2
View File
@@ -21,6 +21,8 @@ jobs:
steps:
- name: Check out the repository
uses: actions/checkout@v4
- name: Prepare the example download
run: python3 scripts/prepare-website.py
- name: Check website links and publishing configuration
run: python3 scripts/verify-website.py
- name: Configure Pages
+3
View File
@@ -45,3 +45,6 @@ Thumbs.db
# Local reference sources and analysis artifacts
/_reference/
# Generated website download
/website/public/examples/notes-example.zip
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "4.1.0"
".": "5.0.0"
}
+259
View File
@@ -1,5 +1,264 @@
# Changelog
## [5.0.0](https://github.com/morluto/rea/compare/rea-agents-4.1.0...rea-agents-5.0.0) (2026-10-07)
### ⚠ BREAKING CHANGES
* **native:** preserve incomplete native UI captures ([#991](https://github.com/morluto/rea/issues/991))
* **android:** Android analysis now requires a full JDK 17 or newer with its compiler module for the packaged Java source bridge.
### Features
* **apple:** project macOS bundle anatomy in application graphs ([88c940b](https://github.com/morluto/rea/commit/88c940b165d788a491e212ea93e00462c4dde6a2))
* **apple:** project macOS bundle anatomy in application graphs ([dd3e7ae](https://github.com/morluto/rea/commit/dd3e7aef0d52c478f6cdf475501c42ebe7f5c97e))
* **application:** wire Android and Apple inventory graph tools ([#852](https://github.com/morluto/rea/issues/852)) ([e0fd820](https://github.com/morluto/rea/commit/e0fd8207a1fb738bcc34f204b55c4fa154f5f02d))
* **browser:** attribute native execution and listener sources ([#917](https://github.com/morluto/rea/issues/917)) ([b5afa5f](https://github.com/morluto/rea/commit/b5afa5f91eed9ae31d670aeab9bbf5112d790ec2))
* **browser:** capture correlated network content and lifecycle evidence ([#797](https://github.com/morluto/rea/issues/797)) ([f9eaf05](https://github.com/morluto/rea/commit/f9eaf056ef58215094513176a70fabae0740081f))
* **browser:** trace captured website module imports ([#879](https://github.com/morluto/rea/issues/879)) ([01be082](https://github.com/morluto/rea/commit/01be082e9e11c78b91828c06a9c543c754783c07))
* **browser:** trace captured website source-map locations ([#897](https://github.com/morluto/rea/issues/897)) ([f430c41](https://github.com/morluto/rea/commit/f430c416c72d444fbe521e0263f8935da4cdb040))
* **javascript:** recover readable modules with Wakaru ([#848](https://github.com/morluto/rea/issues/848)) ([166dcd2](https://github.com/morluto/rea/commit/166dcd21d06f687a9c480a6a6d36beb3b4954dd0))
* **native:** decode chained fixups, binds, ObjC categories and properties ([a657557](https://github.com/morluto/rea/commit/a65755728f7a10d984514c75fa863b84ddcdaee9))
* **native:** decode chained fixups, binds, ObjC categories and properties ([3e24a72](https://github.com/morluto/rea/commit/3e24a72465fe6c628db1f836e93d74d7f4a92b80))
* **native:** open iOS-style and iOS-on-Mac wrapper app bundles ([2ed89e6](https://github.com/morluto/rea/commit/2ed89e6529924248d2e21084f8730bedbb16bc3f))
* **native:** open iOS-style and iOS-on-Mac wrapper app bundles ([aeebba3](https://github.com/morluto/rea/commit/aeebba391ee5c288b75cd828e7d18d32b40aa698))
* **web:** export captured scripts for JavaScript application analysis ([#801](https://github.com/morluto/rea/issues/801)) ([472b72a](https://github.com/morluto/rea/commit/472b72a53068a8e2ec78fe239d687023482daaa7))
* **website:** add a shared back-to-top link ([5047373](https://github.com/morluto/rea/commit/504737300632d8644fbd6c658d2c97e38bb54c3b))
* **website:** introduce REA and the DX-Ball reconstruction ([#906](https://github.com/morluto/rea/issues/906)) ([4e3953c](https://github.com/morluto/rea/commit/4e3953cc45ce9c21786a7a6fc495d2350aa85d24))
### Bug Fixes
* address review nits from batch integration follow-ups ([#818](https://github.com/morluto/rea/issues/818)) ([5a0e0ba](https://github.com/morluto/rea/commit/5a0e0ba3b81ea0ec336addb07da98ec7bc0a35d3))
* **android:** align runtime suffix inference ([#977](https://github.com/morluto/rea/issues/977)) ([fb3749a](https://github.com/morluto/rea/commit/fb3749aff692a7a8225dff00a4516b48a76866b6))
* **android:** bound fragmented JADX MCP frame memory ([#937](https://github.com/morluto/rea/issues/937)) ([9e39795](https://github.com/morluto/rea/commit/9e397958951edb543134e3ad57e7ab253d6df21a))
* **android:** preserve JVM settings and reuse metadata sessions ([#908](https://github.com/morluto/rea/issues/908)) ([eab34a9](https://github.com/morluto/rea/commit/eab34a9d8c5eebad6b04b85750be0276d679c3f9))
* **apple:** attribute only application-root components in macOS inventories ([5524970](https://github.com/morluto/rea/commit/55249704c5c83cbf8d20ac7869863cc196da00a6))
* **apple:** count only real framework version directories ([2e81e86](https://github.com/morluto/rea/commit/2e81e867b24da8314f666f62d154de5803040cdf))
* **apple:** pair bridge hypotheses within one application root ([7acf4e7](https://github.com/morluto/rea/commit/7acf4e741a6a314ea7aabb18fc757e196158ff76))
* **apple:** pair bridge hypotheses within one application root ([9226ade](https://github.com/morluto/rea/commit/9226ade5114f287eb8e1b00b08dd91da1cc66003))
* **apple:** traverse keyed archive hierarchy collections ([#976](https://github.com/morluto/rea/issues/976)) ([71ac83f](https://github.com/morluto/rea/commit/71ac83f3dd376135098f460c86e7897183456ec5))
* **artifacts:** accept APFS image disks ejected with their container ([d93ebec](https://github.com/morluto/rea/commit/d93ebec9df951d40e66ced667f07fadd2dd5d25a))
* **artifacts:** accept APFS image disks ejected with their container ([b72c079](https://github.com/morluto/rea/commit/b72c079a9a90c374bd6dd30ff8c4387306b8f59b))
* **artifacts:** accept the bundle subject when extracting an app bundle ([#891](https://github.com/morluto/rea/issues/891)) ([37d2eba](https://github.com/morluto/rea/commit/37d2ebac4c2a524eec6e32c4c57cbbb632491ad8))
* **artifacts:** decode flat keyed-archive NIBs with data and shared references ([b3f013f](https://github.com/morluto/rea/commit/b3f013fa183e8351319a053575aeee5df337c276))
* **artifacts:** decode flat keyed-archive NIBs with data and shared references ([2a4667b](https://github.com/morluto/rea/commit/2a4667bb79117b202abb009c0bb9ebfdfde57158))
* **artifacts:** harden reader lifecycle boundaries ([#951](https://github.com/morluto/rea/issues/951)) ([2366bda](https://github.com/morluto/rea/commit/2366bda37b1084016d42a0d06920dc57e7c7723f))
* **artifacts:** keep the post-snapshot detach check out of provenance ([62634e4](https://github.com/morluto/rea/commit/62634e4c0177bde8a08decc356da98fbe25d2000))
* **artifacts:** preserve reader failure diagnostics ([#987](https://github.com/morluto/rea/issues/987)) ([fc4a9a0](https://github.com/morluto/rea/commit/fc4a9a05922ce8474fd0717bc800343b0a95bd41))
* **artifacts:** project compiled AppKit actions from the control to its target ([b08228d](https://github.com/morluto/rea/commit/b08228d19f8a0f6dbc940e8d322ef2bb42d2ce1a))
* **artifacts:** project compiled AppKit actions from the control to its target ([#905](https://github.com/morluto/rea/issues/905)) ([900d7c8](https://github.com/morluto/rea/commit/900d7c8606cbc1ccbca818827a55bf2eab28bca4))
* **artifacts:** project UIKit runtime outlet and event connections ([aa8082e](https://github.com/morluto/rea/commit/aa8082ea6c0898feb4d842135aea6b9539f769f8))
* **artifacts:** project UIKit runtime outlet and event connections ([8b24387](https://github.com/morluto/rea/commit/8b243877c22a5550d005e7b8abf93791ce8566b9))
* **artifacts:** read AppKit connectors from flat keyed-archive NIBs ([#900](https://github.com/morluto/rea/issues/900)) ([27c272b](https://github.com/morluto/rea/commit/27c272bd9a42e7235c7991881196c60dbc126714))
* **artifacts:** read keyed archive dictionaries keyed UID as dictionaries ([cfd2b0c](https://github.com/morluto/rea/commit/cfd2b0c75dfb9d51b936788a46d0338145803ece))
* **artifacts:** read keyed archive dictionaries keyed UID as dictionaries ([35c82b9](https://github.com/morluto/rea/commit/35c82b93eb2579687bb446c3a8a2a9f1a7563d28))
* **artifacts:** report which path constraint failed ([#847](https://github.com/morluto/rea/issues/847)) ([3f1a77f](https://github.com/morluto/rea/commit/3f1a77f68ec4355d8b792ccb2970bc03485889c9))
* **browser:** distinguish computed call keys from identifiers ([#808](https://github.com/morluto/rea/issues/808)) ([02beb74](https://github.com/morluto/rea/commit/02beb745358f02bdaabed4bb523c5e3527a9841e))
* **browser:** distinguish WebMCP registration frame owners ([#813](https://github.com/morluto/rea/issues/813)) ([b3b44d8](https://github.com/morluto/rea/commit/b3b44d8a2d8b2aa70a8ed8b9cddf365865fcc53f))
* **browser:** fingerprint cookies at the selected page URL ([#763](https://github.com/morluto/rea/issues/763)) ([78014e3](https://github.com/morluto/rea/commit/78014e3d149f762c848581a0aac7e44f75d51225))
* **browser:** identify NodeNext TypeScript original source artifacts ([#811](https://github.com/morluto/rea/issues/811)) ([e84ad86](https://github.com/morluto/rea/commit/e84ad86a7578fb46568663aca6fe6a6f3986cc8d))
* **browser:** preserve evidence through deep AST analysis ([#924](https://github.com/morluto/rea/issues/924)) ([f826aa8](https://github.com/morluto/rea/commit/f826aa893fa7d55e87d907f0c94c39421a166dd8))
* **browser:** preserve form destinations and satisfies mutation uncertainty ([#825](https://github.com/morluto/rea/issues/825)) ([6dd719e](https://github.com/morluto/rea/commit/6dd719eaf605b37cd5dad6d01d33527ca9d59f06)), closes [#820](https://github.com/morluto/rea/issues/820)
* **browser:** preserve Link header value delimiters ([#805](https://github.com/morluto/rea/issues/805)) ([dd36405](https://github.com/morluto/rea/commit/dd3640548fe3d1e8b92784a63fac55d757dcc1f0))
* **build:** validate installed dependencies before cache hits ([#982](https://github.com/morluto/rea/issues/982)) ([a45fe86](https://github.com/morluto/rea/commit/a45fe86a39b9e7a7c21e3665b6de4763cb72d782))
* capture IndexedDB object-store record fingerprints ([#809](https://github.com/morluto/rea/issues/809)) ([21a8bd0](https://github.com/morluto/rea/commit/21a8bd0ccde2326c8bc022714a88a438d8b89dde))
* **cli:** align structured output guard with native flags ([#777](https://github.com/morluto/rea/issues/777)) ([e9fac56](https://github.com/morluto/rea/commit/e9fac56232e74c8287863ca5c4cc7d8e39fb4a40))
* **cli:** emit empty structured filter projections ([#914](https://github.com/morluto/rea/issues/914)) ([2db668f](https://github.com/morluto/rea/commit/2db668f6ca91d2514a354fd459239b84031812e7))
* **cli:** honor the selected native plist path ([#773](https://github.com/morluto/rea/issues/773)) ([0374865](https://github.com/morluto/rea/commit/0374865836aa4960657fb92f588c71e35dd3bed8))
* **cli:** preserve output and comparison input validation ([#981](https://github.com/morluto/rea/issues/981)) ([d5c43d6](https://github.com/morluto/rea/commit/d5c43d65f0f6a1de8fb2eb5f2c1940a6ea2af20c))
* **cli:** preserve unsupported historical-source imports ([#795](https://github.com/morluto/rea/issues/795)) ([911d580](https://github.com/morluto/rea/commit/911d58063477912042da5e9e573a9e8008956622))
* **contracts:** align CLI/MCP boundary contracts ([#946](https://github.com/morluto/rea/issues/946)) ([4a60738](https://github.com/morluto/rea/commit/4a6073810a955d058c10f209157cbd68832a92c5))
* **deps:** remediate smol-toml advisories ([93091ab](https://github.com/morluto/rea/commit/93091abb4dbd3cceee217d179db816895df24d70))
* **deps:** remediate smol-toml advisories ([c55ae3b](https://github.com/morluto/rea/commit/c55ae3b9eccc17e6e8f4369e13044995d0650704))
* **dotnet:** compare members with undecoded signatures by exact identity ([28fd49f](https://github.com/morluto/rea/commit/28fd49f0f6cf54e373411f5999e6b5722dd826f5))
* **dotnet:** compare members with undecoded signatures by exact identity ([d52af3b](https://github.com/morluto/rea/commit/d52af3b321a321964598fb302edeb26552c9dffd))
* **dotnet:** inspect native boundaries of non-managed and malformed PEs ([d5f28fb](https://github.com/morluto/rea/commit/d5f28fb29c5c3e8acef940fcae042e660e3a4d72))
* **dotnet:** inspect native boundaries of non-managed and malformed PEs ([d98dcfa](https://github.com/morluto/rea/commit/d98dcfa8c2e46a7cc918aeb01058c26c4a84060c))
* **dotnet:** keep a decoded member unknown beside an undecoded counterpart ([76b7020](https://github.com/morluto/rea/commit/76b70203d941ab85c1772f330407fd832d438c9c))
* **dotnet:** keep admitted CLI header facts when metadata is unreadable ([e5ba693](https://github.com/morluto/rea/commit/e5ba6934e4ff819c09077ca70bd2417d37f85bc0))
* **dotnet:** name the shared key in exact-signature ambiguities ([7714e76](https://github.com/morluto/rea/commit/7714e763d9ac2e87994068b3dee37ca6c5ffc6f4))
* **dotnet:** treat ambiguous undecoded members as possible counterparts ([73370e1](https://github.com/morluto/rea/commit/73370e1c064899753988581c9a821e0e7f579561))
* **errors:** report union input issues at the union's path ([bddeb63](https://github.com/morluto/rea/commit/bddeb634b583cf4f7d5fa6cf3a1ee821e05d2923))
* **evaluation:** canonicalize Unicode argument keys ([#980](https://github.com/morluto/rea/issues/980)) ([0a4caf4](https://github.com/morluto/rea/commit/0a4caf4a0a1355b6a121fc88886b22e55843ac76))
* **firmware:** accept the verified Binwalk and Unblob release lines ([#844](https://github.com/morluto/rea/issues/844)) ([bef4321](https://github.com/morluto/rea/commit/bef4321afb3d0d480e8a04ad7e256663542caa3d))
* **firmware:** retain multi-file extraction failures ([#978](https://github.com/morluto/rea/issues/978)) ([8aaf9a1](https://github.com/morluto/rea/commit/8aaf9a19788beb6612332505850ad3407cb00027))
* **ghidra:** admit the 12.1 line and the install's JDK range ([#840](https://github.com/morluto/rea/issues/840)) ([5cbf06c](https://github.com/morluto/rea/commit/5cbf06c7ad0ace599721bffd7da566b8a790b622))
* **ghidra:** select exact packaged bridge scripts ([#800](https://github.com/morluto/rea/issues/800)) ([894c30e](https://github.com/morluto/rea/commit/894c30ef02d946f0c8de3ae6c9168d9525d95033))
* **ida:** share concurrent session cleanup ([#950](https://github.com/morluto/rea/issues/950)) ([f14b6a8](https://github.com/morluto/rea/commit/f14b6a84306116716e78c5ffc84353ac07939593))
* **installer:** validate exact semantic versions ([#954](https://github.com/morluto/rea/issues/954)) ([248972f](https://github.com/morluto/rea/commit/248972f327776687037dc07099117584c0674e77))
* integrate 24 reviewed browser/native/semantics corrections ([#815](https://github.com/morluto/rea/issues/815)) ([3af3811](https://github.com/morluto/rea/commit/3af3811ab0a212b6aab6185628d47ecad37958fc))
* **javascript:** advertise the exact empty seed rule and match empty API keys ([35e5508](https://github.com/morluto/rea/commit/35e5508fd095a062d7477bd6bb7d2d8e28201cd8))
* **javascript:** analyze source maps with empty source names ([58ef8ac](https://github.com/morluto/rea/commit/58ef8ac3aa803d7b64dd12a5b2a2e8ec24261060))
* **javascript:** analyze sources containing empty string literals ([e7cbfe0](https://github.com/morluto/rea/commit/e7cbfe04fdfd7affe50dba9e2c7a9a0a72291041))
* **javascript:** analyze sources containing empty string literals ([11fc3b9](https://github.com/morluto/rea/commit/11fc3b981509e8107b10b2e75363f5bb489879db))
* **javascript:** filter keyed-collection receivers only for get/delete ([00f707b](https://github.com/morluto/rea/commit/00f707b250451246d69a1205c39c6843fab6bb5b))
* **javascript:** handle deep AST traversal and addition without stack overflow ([#915](https://github.com/morluto/rea/issues/915)) ([62b3711](https://github.com/morluto/rea/commit/62b37111e7f09887eb712c6e8e721e0334cf10b9))
* **javascript:** handle deep static member paths ([#939](https://github.com/morluto/rea/issues/939)) ([348c65c](https://github.com/morluto/rea/commit/348c65c7ece1aee280f9431d995996b444e7549d))
* **javascript:** keep bare relative URLs for computed-method XHR ([e32028d](https://github.com/morluto/rea/commit/e32028d1b566872ba8cd08fc6800c198d6aef30b))
* **javascript:** keep computed-method XHR request URLs ([602d002](https://github.com/morluto/rea/commit/602d0021d45a6cdab9f806554f3ee12ea670ec97))
* **javascript:** keep exact empty env keys and method names ([522c2d0](https://github.com/morluto/rea/commit/522c2d0ecb1eb7be262eaef2926cceac9a68badc))
* **javascript:** keep exact empty env keys and method names ([f7954a8](https://github.com/morluto/rea/commit/f7954a8fd692f262ef3ce1507b957681150ce9c4)), closes [#783](https://github.com/morluto/rea/issues/783)
* **javascript:** keep exact empty require paths and request fields ([ad1f6b5](https://github.com/morluto/rea/commit/ad1f6b508fbdd44d8bf1884ed96ae69082808e7d)), closes [#783](https://github.com/morluto/rea/issues/783)
* **javascript:** keep exact empty values in graph identities ([f608127](https://github.com/morluto/rea/commit/f608127635ed8e0d105ad95252e5ea0dff899f5b))
* **javascript:** keep exact empty values in graph identities ([63012ee](https://github.com/morluto/rea/commit/63012eeb601fc74ca82f3a027d852a5da1b099d2))
* **javascript:** keep keyed-collection reads out of endpoints ([9075ce7](https://github.com/morluto/rea/commit/9075ce71ada4334407aa2d194dc82e3031e30260))
* **javascript:** keep keyed-collection reads out of endpoints ([eb68761](https://github.com/morluto/rea/commit/eb68761eb0a35b876e33dd6115d0d07f97e99dd5))
* **javascript:** keep literal HTTP methods on storage-named receivers ([9f48a76](https://github.com/morluto/rea/commit/9f48a76e1186cc43c6928eff1c82e93280caee1a))
* **javascript:** keep literal-method and URL-literal requests on aliased receivers ([e3e7664](https://github.com/morluto/rea/commit/e3e76646657e950e24241ba715eb97ea385933e9))
* **javascript:** keep mutated object initializer values unknown ([#752](https://github.com/morluto/rea/issues/752)) ([6caa38a](https://github.com/morluto/rea/commit/6caa38a0cad369ddff52b08f8d7d44c1b92ecac0))
* **javascript:** keep storage open versions out of XHR endpoints ([4f51103](https://github.com/morluto/rea/commit/4f5110306d9edab94bf63bf81cf259324d96af57))
* **javascript:** keep storage open versions out of XHR endpoints ([898b0f6](https://github.com/morluto/rea/commit/898b0f62d76ea1cf793140cb255cad16ab85b44d))
* **javascript:** keep XHR and client endpoints that receiver spellings hid ([05f43ed](https://github.com/morluto/rea/commit/05f43ed388ae4e9316d320b592b6eba5b119ce89))
* **javascript:** leave empty application values unlabeled ([527b76e](https://github.com/morluto/rea/commit/527b76ef78e84d29bb41bd45f1f83fae05ca66ca))
* **javascript:** match exact empty literal channels and seeds ([e20da1f](https://github.com/morluto/rea/commit/e20da1f205b105b1b6a5decd3c2d0fe36fd12410))
* **javascript:** never read window or document open as XHR ([ae849a7](https://github.com/morluto/rea/commit/ae849a72811857ba9755962ec48a1d76c9d1181a))
* **javascript:** preserve computed Electron member uncertainty ([3a4a63f](https://github.com/morluto/rea/commit/3a4a63f70a2e42fb1fa48881e08b08d612289f2e))
* **javascript:** preserve computed Electron member uncertainty ([1260cac](https://github.com/morluto/rea/commit/1260cacc67b8e0c93fb9434452053feca956e7e7))
* **javascript:** project decoded file URL paths ([#810](https://github.com/morluto/rea/issues/810)) ([2572e1e](https://github.com/morluto/rea/commit/2572e1eb2f853a4cebfc8303d9e293e1d106d608))
* **javascript:** read open() endpoints only from XHR method/URL pairs ([63e0822](https://github.com/morluto/rea/commit/63e0822cc794bd6ec1688340b3230ffb6b5c4a8c))
* **javascript:** read open() endpoints only from XHR method/URL pairs ([1be3705](https://github.com/morluto/rea/commit/1be370543f7ea39016f86ebdec80a8ceaf7f8a28))
* **javascript:** read substitution-free templates as exact strings ([c371d6e](https://github.com/morluto/rea/commit/c371d6e1e9cceb28e6cfc874e14c4e740bb87751))
* **javascript:** read substitution-free templates as exact strings ([77cf757](https://github.com/morluto/rea/commit/77cf75710369acc6c304ec352ab25aa7274e04b1))
* **javascript:** resolve bare source-map and worker URLs next to the script ([#890](https://github.com/morluto/rea/issues/890)) ([45fcd28](https://github.com/morluto/rea/commit/45fcd28b4018723bdb508a9229b74ab9eb749f75))
* **javascript:** retain recovered call argument positions ([#929](https://github.com/morluto/rea/issues/929)) ([e6ba9c3](https://github.com/morluto/rea/commit/e6ba9c3af3053485f154825eea6a156127328be9))
* **linux:** improve Hopper reliability and CachyOS support ([#963](https://github.com/morluto/rea/issues/963)) ([98b2b02](https://github.com/morluto/rea/commit/98b2b027afd8c80b68e9daaf97bdaa4d942105cc))
* **mcp:** advertise reserved process environment key ([#949](https://github.com/morluto/rea/issues/949)) ([e0d76ae](https://github.com/morluto/rea/commit/e0d76ae80ddc70ae4d5afcbd1427afb81eee3669))
* **mcp:** isolate optional observation adapter failures ([#838](https://github.com/morluto/rea/issues/838)) ([be3cfbf](https://github.com/morluto/rea/commit/be3cfbffdfbbb65da75dcec0518e2ebf387e66bc))
* **mcp:** preserve exact guided prompt context ([#761](https://github.com/morluto/rea/issues/761)) ([66c26f2](https://github.com/morluto/rea/commit/66c26f280b1d494806ee182b52564166760f0ae2))
* **native-ui:** handle incomplete accessibility child collections ([#944](https://github.com/morluto/rea/issues/944)) ([ac3bae3](https://github.com/morluto/rea/commit/ac3bae3cb97c4fb622eb1b6ac1f3b84e6a70ba03))
* **native:** inspect plists containing data, dates, and non-finite reals ([bc2cd8b](https://github.com/morluto/rea/commit/bc2cd8b874e115eee446860043758a80bd583ad0))
* **native:** inspect plists containing data, dates, and non-finite reals ([5ead2cf](https://github.com/morluto/rea/commit/5ead2cfbc47896b43502363d3568330f5d94762e))
* **native:** keep echoed codesign paths out of signature fields ([#865](https://github.com/morluto/rea/issues/865)) ([4aa2b60](https://github.com/morluto/rea/commit/4aa2b60c1d84607a138395ae93260b2e50c2a721))
* **native:** keep echoed Mach-O paths out of load commands and symbols ([ab985c6](https://github.com/morluto/rea/commit/ab985c6a10e9da856ed151100cee8459793c3573))
* **native:** keep echoed Mach-O paths out of load commands and symbols ([37fe105](https://github.com/morluto/rea/commit/37fe105ba168488d05226f2912bed802e2d48d75))
* **native:** keep integral plist reals beyond the JSON range as reals ([#860](https://github.com/morluto/rea/issues/860)) ([3b92a2b](https://github.com/morluto/rea/commit/3b92a2bceddaa1659654da08160813a3affbc6b7))
* **native:** keep plist XML element types for large numbers ([f85056d](https://github.com/morluto/rea/commit/f85056d83e42a461c8c947792774580294d0ceb9))
* **native:** keep the resolved bundle plist and permission failures ([418a556](https://github.com/morluto/rea/commit/418a5562f6f640503c751924a4787586c2c5c98a))
* **native:** pass Swift symbols to swift-demangle as operands ([dd90965](https://github.com/morluto/rea/commit/dd90965058a9e6f7dd7f30eb7e462572e777db7f))
* **native:** pass Swift symbols to swift-demangle as operands ([6c88094](https://github.com/morluto/rea/commit/6c88094da45258b346fe594764e561f999622a58))
* **native:** preserve incomplete native UI captures ([#991](https://github.com/morluto/rea/issues/991)) ([9f496ef](https://github.com/morluto/rea/commit/9f496ef2d4a3aa68816287bf5d2ba1620d72c97a))
* **native:** report exact plist integers beyond the JSON number range ([c9f49db](https://github.com/morluto/rea/commit/c9f49db907db0513ad8b22c9b2bbdcee400aff11))
* **native:** report exact plist integers beyond the JSON number range ([430c297](https://github.com/morluto/rea/commit/430c297ca4ddb3491c2b835b889486e6a2ffdc17))
* **native:** report plist permission denials as access failures ([5738a0c](https://github.com/morluto/rea/commit/5738a0cf8c111833847fecb2f7d0c2ee35eabbd6))
* **native:** report why a selected plist cannot be inspected ([14f9b3d](https://github.com/morluto/rea/commit/14f9b3d93119a60a3cf5f3d9c138ca829a5462e8))
* **native:** report why a selected plist cannot be inspected ([0674fac](https://github.com/morluto/rea/commit/0674facd468c3321584ece908031feee7220b9c6))
* **plist:** keep character references beyond Unicode literal ([5afd8a8](https://github.com/morluto/rea/commit/5afd8a850b2ebd62e3400a5e2ece1a1cfa98bab6))
* **plist:** read XML plists containing a __proto__ key ([faaee69](https://github.com/morluto/rea/commit/faaee693d5bb4bb957387ebd0f4338513c4c914a))
* **plist:** read XML plists containing a __proto__ key ([a08be51](https://github.com/morluto/rea/commit/a08be51e9f0d23bd46b66bbdb88dbda44bc54c88))
* **plist:** report omitted __proto__ entries in signatures and Interface Builder ([15ab5db](https://github.com/morluto/rea/commit/15ab5dbcaf3fac09756e98926373965247dad622))
* **plist:** report omitted __proto__ entries in signatures and Interface Builder ([6729ee7](https://github.com/morluto/rea/commit/6729ee7636889a0a2ebc9fca8e35860476f8ca34))
* preserve native UI failure reasons and dedupe version literals ([#854](https://github.com/morluto/rea/issues/854)) ([2d4a910](https://github.com/morluto/rea/commit/2d4a910fcd30e8d5b96fcb957b931e9aa3e4ab0c))
* **process:** clarify Windows capture support and recovery ([#798](https://github.com/morluto/rea/issues/798)) ([50787b2](https://github.com/morluto/rea/commit/50787b2faf1a4bea799d041960ca2b91199ef403))
* **process:** harden filesystem snapshot identity and cancellation ([#956](https://github.com/morluto/rea/issues/956)) ([93aa9eb](https://github.com/morluto/rea/commit/93aa9eb5c2551984182a4025d836e4223e8ecc8a))
* **process:** retain provider output until streams close ([#928](https://github.com/morluto/rea/issues/928)) ([b929251](https://github.com/morluto/rea/commit/b9292516f4eedc2dedba713abf57f665ef1fb596))
* **reference:** retain partial imports for unreadable directories ([#766](https://github.com/morluto/rea/issues/766)) ([cffc548](https://github.com/morluto/rea/commit/cffc548cd3127c5db811f20db4816039846b3170))
* **release:** drive checkpoint releases from frozen branches ([fc58fa1](https://github.com/morluto/rea/commit/fc58fa1eb01fe5a8200a78e9551799c9c1a3d256))
* **release:** publish from explicit source checkpoints ([49887f0](https://github.com/morluto/rea/commit/49887f0239da4483153a5e1610058619855525db))
* **semantics:** isolate named function expression bindings ([#776](https://github.com/morluto/rea/issues/776)) ([ae81f64](https://github.com/morluto/rea/commit/ae81f64d2ad10e5cdbf8356819351c02480aee7a))
* **semantics:** preserve direct promise ownership boundaries ([#758](https://github.com/morluto/rea/issues/758)) ([e1ca412](https://github.com/morluto/rea/commit/e1ca412757bb01be855a66bf72006e3becd497b6))
* **semantics:** preserve empty string property keys ([#764](https://github.com/morluto/rea/issues/764)) ([630bdaf](https://github.com/morluto/rea/commit/630bdafcacc621105f6a43ed68a6e51651aca629))
* **semantics:** recognize Node builtin default namespaces ([#769](https://github.com/morluto/rea/issues/769)) ([24a0bf9](https://github.com/morluto/rea/commit/24a0bf9a618604e2fa30817d673ce911c7842819))
* **semantics:** retain trailing rest argument flows ([#756](https://github.com/morluto/rea/issues/756)) ([4bfd803](https://github.com/morluto/rea/commit/4bfd8038be0a60c43a452bec15e23799a0b040c0))
* **setup:** accept the pinned Hopper RPM launcher ([#933](https://github.com/morluto/rea/issues/933)) ([dda8e6a](https://github.com/morluto/rea/commit/dda8e6a72890f87bdf8bbd3250b68b6648aae553))
* **setup:** read an OpenCode V2 server named type as a table entry ([#866](https://github.com/morluto/rea/issues/866)) ([4e17be5](https://github.com/morluto/rea/commit/4e17be57053fb0ce2561d92e05b6e38ef789b2d4))
* **setup:** register OpenCode V2 configurations in mcp.servers ([957069d](https://github.com/morluto/rea/commit/957069d14d832ddeac3f90dd141d379b81e2832a))
* **setup:** register OpenCode V2 configurations in mcp.servers ([69d8afe](https://github.com/morluto/rea/commit/69d8afefc19e014a4bba7a182983a9e77e100a75))
* **target:** decode app Info.plist executables with an XML parser ([c57f1b7](https://github.com/morluto/rea/commit/c57f1b74e0af5cf5fbb459cb247e1448cbf8df1b))
* **target:** decode app Info.plist executables with an XML parser ([c182f72](https://github.com/morluto/rea/commit/c182f72ad3d9eea10b084a43527acb7c8ca2d6bf))
* **test:** disconnect Hopper fixture IPC on SIGTERM ([b912b11](https://github.com/morluto/rea/commit/b912b117f019d5d3e4b4e3ef683e9b25f3a709bc))
* **windows:** accept ordinary drive path separators ([#791](https://github.com/morluto/rea/issues/791)) ([b62931f](https://github.com/morluto/rea/commit/b62931f8335f58bfe344883a4f149e9eef73d835))
* **windows:** remove cancelled snapshot outputs ([#957](https://github.com/morluto/rea/issues/957)) ([d76f7d1](https://github.com/morluto/rea/commit/d76f7d150895289aa1fb85a057b81ca7ff102df4))
### Performance Improvements
* **artifacts:** stream ASAR members without full buffering ([#962](https://github.com/morluto/rea/issues/962)) ([abd7a5e](https://github.com/morluto/rea/commit/abd7a5ed091061e4b1e83e5933e9009fb5b8d577))
* **call-path:** prune dead ends and enumerate without recursion ([#909](https://github.com/morluto/rea/issues/909)) ([e52fe97](https://github.com/morluto/rea/commit/e52fe9719fec03f2aa79b1a1b9e11b38c229903c))
* **cli:** defer catalog digests and use canonical cache scope ([9a15525](https://github.com/morluto/rea/commit/9a1552529889e9f5b52a88739eb767b92e57e24e))
* **cli:** defer unused catalog work during command startup ([#920](https://github.com/morluto/rea/issues/920)) ([9a15525](https://github.com/morluto/rea/commit/9a1552529889e9f5b52a88739eb767b92e57e24e))
* **comparison:** reuse canonical sort keys ([#990](https://github.com/morluto/rea/issues/990)) ([ebb065e](https://github.com/morluto/rea/commit/ebb065e2dc15f5763276f401521896f66bae6339))
* **dotnet:** index declaring-type ranges once per metadata table ([#918](https://github.com/morluto/rea/issues/918)) ([7a9f587](https://github.com/morluto/rea/commit/7a9f587c3a9998d828b83388438976527ca7074f))
* **dotnet:** index ordered declaring-type ranges once per table ([7a9f587](https://github.com/morluto/rea/commit/7a9f587c3a9998d828b83388438976527ca7074f))
* **evidence:** scale imports and dependency graphs ([#941](https://github.com/morluto/rea/issues/941)) ([7dc40d3](https://github.com/morluto/rea/commit/7dc40d3d9e00caf013109942a5d9a1d7fdf9ec62))
* **inspector:** bound runtime script location authorization ([#945](https://github.com/morluto/rea/issues/945)) ([58381ee](https://github.com/morluto/rea/commit/58381eefa24f73580e31eaa09dc14b9c04d78d6c))
* **javascript:** analyze member chains without recursive copying ([#932](https://github.com/morluto/rea/issues/932)) ([dcde958](https://github.com/morluto/rea/commit/dcde9584508830a5d99ac5e2032f539563c69002))
* **javascript:** index Promise return-site ownership ([#971](https://github.com/morluto/rea/issues/971)) ([2cedde9](https://github.com/morluto/rea/commit/2cedde9f1cf72b04bef37f9f004bb68d5ff6502a))
* **javascript:** index semantic fingerprint relations ([#942](https://github.com/morluto/rea/issues/942)) ([c92a344](https://github.com/morluto/rea/commit/c92a344d4770125d85a11f3fc7a23d800bd30003))
* **javascript:** reduce analysis and source-map work ([#955](https://github.com/morluto/rea/issues/955)) ([c7530f9](https://github.com/morluto/rea/commit/c7530f9a816d84354516d3ea137f4a0d91d3b8b3))
* **providers:** assemble fragmented responses once per line ([#911](https://github.com/morluto/rea/issues/911)) ([df7151b](https://github.com/morluto/rea/commit/df7151ba305eab3a33a9261a023705b29862b308))
* **reconstruction:** index coverage evaluation ([#983](https://github.com/morluto/rea/issues/983)) ([f26c8b5](https://github.com/morluto/rea/commit/f26c8b5b8cff10b93173359b5453874d659f2440))
* **setup:** reuse initial diagnostics during planning ([#940](https://github.com/morluto/rea/issues/940)) ([118d43e](https://github.com/morluto/rea/commit/118d43e787e38857714852cc846b1b604a61950d))
### Code Refactoring
* add incremental capability migration safeguards ([#819](https://github.com/morluto/rea/issues/819)) ([75f50d8](https://github.com/morluto/rea/commit/75f50d86f6d504f4dd97a707871d39ec022b3047))
* **android:** group layered source and verification ([#873](https://github.com/morluto/rea/issues/873)) ([8e8921d](https://github.com/morluto/rea/commit/8e8921d40c63b40ab7e1598f672063dd304d93f0))
* **apple:** group artifact producers and verification ([#952](https://github.com/morluto/rea/issues/952)) ([ef1a0cc](https://github.com/morluto/rea/commit/ef1a0cc65af7918ef7204a39f42a4299b486c453))
* **artifacts:** isolate JavaScript file acquisition ([#916](https://github.com/morluto/rea/issues/916)) ([f0a68dc](https://github.com/morluto/rea/commit/f0a68dce60e81b4d2f420bf774b61957a37326ac))
* **binary:** group session selection and snapshot ownership ([#886](https://github.com/morluto/rea/issues/886)) ([dd114d3](https://github.com/morluto/rea/commit/dd114d39801973e958a582c8fcba7ccce0e50299))
* **binary:** move production composition outside application workflows ([#965](https://github.com/morluto/rea/issues/965)) ([33d4761](https://github.com/morluto/rea/commit/33d4761cc7a96630b7c2fed890df83d433c45914))
* **browser:** align inspector exclusions with canonical vocabulary ([#834](https://github.com/morluto/rea/issues/834)) ([de9593a](https://github.com/morluto/rea/commit/de9593ad48c0b97507f18949b7ad86ad9c743ebc))
* **cli:** split core analysis command registrar ([#858](https://github.com/morluto/rea/issues/858)) ([8b9940a](https://github.com/morluto/rea/commit/8b9940a7b5d3fcd5a8eda6d397f0ab7c3aaf2ac9))
* colocate investigation record implementation ([#867](https://github.com/morluto/rea/issues/867)) ([bb19c94](https://github.com/morluto/rea/commit/bb19c9477ef3eefd9c5b2e6cabe78cc94e6779ae))
* compose investigation record ownership ([#845](https://github.com/morluto/rea/issues/845)) ([ce73c3f](https://github.com/morluto/rea/commit/ce73c3fed2e5fd20ae0113579345a69763eb7655))
* declare auxiliary capabilities from source ([#853](https://github.com/morluto/rea/issues/853)) ([4a6c17c](https://github.com/morluto/rea/commit/4a6c17ce83dfd20412f0e168525314a20c61169e))
* **domain:** consolidate property-key extraction behind semanticStaticPropertyKey ([#823](https://github.com/morluto/rea/issues/823)) ([764ff05](https://github.com/morluto/rea/commit/764ff05231f1a666dfa655492da42b4b6ba742c3))
* **domain:** converge comparison mechanics on shared vocabulary ([#824](https://github.com/morluto/rea/issues/824)) ([c13c9d4](https://github.com/morluto/rea/commit/c13c9d41f8441a711266adee21429fb7433a6aa1))
* **domain:** extract artifact path syntax helpers ([#830](https://github.com/morluto/rea/issues/830)) ([ef535b8](https://github.com/morluto/rea/commit/ef535b81fefc358b0bdb655e5aa057fc3949ce5f))
* **domain:** extract source-map envelope walk and disambiguate stringValue ([#828](https://github.com/morluto/rea/issues/828)) ([64ebdf4](https://github.com/morluto/rea/commit/64ebdf45016bda679674cbd2287576eadf66a1fc))
* **domain:** map validation taxonomies and add inventory Result seam ([#833](https://github.com/morluto/rea/issues/833)) ([78f4848](https://github.com/morluto/rea/commit/78f4848d70ce462426e9f221d6e7b3e34219bf46))
* **domain:** split Interface Builder keyed-archive parse from graph builder ([#859](https://github.com/morluto/rea/issues/859)) ([da09706](https://github.com/morluto/rea/commit/da0970672da725dbb095ffcaefad07303ae85aab))
* **firmware:** group layered source and verification ([#878](https://github.com/morluto/rea/issues/878)) ([507e8b3](https://github.com/morluto/rea/commit/507e8b3c7be75f8de69be214618394aca3585404))
* **ghidra:** extract createClient and load-image attestation ([#856](https://github.com/morluto/rea/issues/856)) ([b7df5b7](https://github.com/morluto/rea/commit/b7df5b78052539e57967db8e6562befd70c6ebdc))
* give Inspector its own capability directory ([#869](https://github.com/morluto/rea/issues/869)) ([021ba69](https://github.com/morluto/rea/commit/021ba690b07eab4b990be47f90946db9f70e0fc0))
* **javascript:** group application workflows and verification ([#896](https://github.com/morluto/rea/issues/896)) ([0804722](https://github.com/morluto/rea/commit/0804722cdf721a8d767d02f9342ebdf8d186f7e1))
* **javascript:** group domain semantics and contracts ([#907](https://github.com/morluto/rea/issues/907)) ([f6a58f1](https://github.com/morluto/rea/commit/f6a58f10629614fb7a6970421218b73d41bce6a5))
* **managed:** group capability workflows and verification ([#926](https://github.com/morluto/rea/issues/926)) ([28713f9](https://github.com/morluto/rea/commit/28713f9b4dcb882f0e811653890fde70cb5fdadb))
* **native:** group analyst semantics workflows and contracts ([#973](https://github.com/morluto/rea/issues/973)) ([ea4db4c](https://github.com/morluto/rea/commit/ea4db4c9f7cbc10dc701c4d5770109c08952a5b8))
* **native:** split Apple dispatch metadata decode facets ([#855](https://github.com/morluto/rea/issues/855)) ([676959e](https://github.com/morluto/rea/commit/676959ecb55b606a567af3d2eb3d3edc96f0aa2f))
* **provider:** declare deep capabilities from source ([#892](https://github.com/morluto/rea/issues/892)) ([b808e2d](https://github.com/morluto/rea/commit/b808e2d6bbbae38c1ff2e293e1cf77464d889bdd))
* share Android and firmware provider factories ([#826](https://github.com/morluto/rea/issues/826)) ([3ed59b2](https://github.com/morluto/rea/commit/3ed59b281247f2180b3c79e658078507e4cc1be2))
### Documentation
* add concise lawful-use disclaimer ([#888](https://github.com/morluto/rea/issues/888)) ([4037c65](https://github.com/morluto/rea/commit/4037c6545dc8cc47f4602d8f06e5329ed641cdbb))
* add DX-Ball reconstruction showcase ([#876](https://github.com/morluto/rea/issues/876)) ([895079d](https://github.com/morluto/rea/commit/895079dd093f02487789d9aa967f0cccbfe379d8))
* add prominent website and showcase links to READMEs ([2ccc86c](https://github.com/morluto/rea/commit/2ccc86cd13d71f08b553d3b9962175144b882539))
* add README star history chart ([#850](https://github.com/morluto/rea/issues/850)) ([11f27de](https://github.com/morluto/rea/commit/11f27deddf88703368942d3ddfc239a7ebe72e3e))
* add website and showcase links to READMEs ([#930](https://github.com/morluto/rea/issues/930)) ([2ccc86c](https://github.com/morluto/rea/commit/2ccc86cd13d71f08b553d3b9962175144b882539))
* **apple:** describe directory-based framework plist ambiguity ([6d74992](https://github.com/morluto/rea/commit/6d74992efed3271e8370c505afdde347609f8353))
* audit and improve localized readmes translations and wording ([00a6ac3](https://github.com/morluto/rea/commit/00a6ac3de99f432bd6dbb88148a2aab5f8d3d338))
* clarify NativeAOT scope and regenerate conformance ledger ([c945931](https://github.com/morluto/rea/commit/c945931961cbd488163c6d92ef91336ed506942e))
* fix stale IDA roadmap item, mermaid paths, toolchain wording ([d2aed17](https://github.com/morluto/rea/commit/d2aed172ab79d98ff4e62c14d8f34cc49c1d17b4))
* move quick start up and mark skill install optional ([554e9b1](https://github.com/morluto/rea/commit/554e9b1612f7799c2bc7177eb1a9b04a0f27d2e9))
* note Cloud Agent Node.js toolchain ([#837](https://github.com/morluto/rea/issues/837)) ([6aa6350](https://github.com/morluto/rea/commit/6aa6350c800796d0953937e894ff176ae248e930))
* publish VitePress documentation on GitHub Pages ([c78fe69](https://github.com/morluto/rea/commit/c78fe6990bcface6853ac5c11c90df6bc0df562d))
* separate ambiguous and unavailable provider recovery ([c3f2b8e](https://github.com/morluto/rea/commit/c3f2b8e7742915dc27ab7c801a8a1a9127169896))
* show task-scoped readiness and provider preference in the quick start ([880cca1](https://github.com/morluto/rea/commit/880cca1bca497bb53c97a821b470df0f2235e043))
* show task-scoped readiness and provider preference in the quick start ([5fd0db3](https://github.com/morluto/rea/commit/5fd0db37d94f1c1a0495920c79e121d799da0d30)), closes [#726](https://github.com/morluto/rea/issues/726)
* update release boundary to npm 4.1.0 with Windows native bundle ([69be514](https://github.com/morluto/rea/commit/69be514c67c8b2eb8b450355a9782d9237748fda))
* **website:** add Notion case study and improve onboarding ([a6ba772](https://github.com/morluto/rea/commit/a6ba772c08e2648e6602787a0ea537d75d72802d))
* **website:** add TH04 bullet-ring case study ([f8d84da](https://github.com/morluto/rea/commit/f8d84da127fad871050c098b004a503b36795ad0))
* **website:** add worked guides and keep Pages publishing manual ([#925](https://github.com/morluto/rea/issues/925)) ([4f25ee3](https://github.com/morluto/rea/commit/4f25ee33dcd4195f8d877f8835f81c4e08c95235))
* **website:** clarify guides and case studies ([5065fcd](https://github.com/morluto/rea/commit/5065fcd5205ec7c0d259131431c2cf54d9f69cf6))
### Tests
* **artifacts:** report compiled connector records when a storyboard route is missing ([d421d3f](https://github.com/morluto/rea/commit/d421d3f286ce515b5126859a0f9cac60667a168e))
* **artifacts:** report observed action routes when a compiled route is missing ([c60b737](https://github.com/morluto/rea/commit/c60b737f7db8e2c9a1982cdb4357fcda1f47ff08))
* **artifacts:** write the storyboard fixture's action in Cocoa syntax ([53bfa7e](https://github.com/morluto/rea/commit/53bfa7e3f792cdb9f2e95fc954f3616426414bcc))
* eliminate fixture races and prune redundant scaffolding ([#849](https://github.com/morluto/rea/issues/849)) ([f72931d](https://github.com/morluto/rea/commit/f72931d9dd54609bfca422750fc2667f80af691c))
### Continuous Integration
* consolidate macOS native baseline checks ([#935](https://github.com/morluto/rea/issues/935)) ([810fe53](https://github.com/morluto/rea/commit/810fe53e799b89dc215c52c9245d36fb2a44d9c4))
* verify native arm64 and Intel macOS workflows ([#898](https://github.com/morluto/rea/issues/898)) ([4801a24](https://github.com/morluto/rea/commit/4801a24089964860db7f4855be21d2d4313c7eab))
## [4.1.0](https://github.com/morluto/rea/compare/rea-agents-4.0.1...rea-agents-4.1.0) (2026-10-06)
+11 -29
View File
@@ -10,7 +10,7 @@ When adding or changing an MCP tool, follow the [tool design guide](docs/tool-de
## Development setup
REA development requires Node.js 24.18.x and npm 11.16.x (pinned toolchain via `nvm use`; the supported runtime range is Node.js ^22.19 || ^24.11 || >=26, as the README badge states). Real-Hopper verification additionally requires either macOS 12+ or an officially supported Linux host (Ubuntu 24.04+, Fedora 41+, or 64-bit Arch) and an installed Hopper application. Linux demo verification uses its own private Xvfb display and does not require a desktop session. Run `nvm use` before installing dependencies.
REA development requires Node.js 24.18.x and npm 11.16.x (pinned toolchain via `nvm use`; the supported runtime range is Node.js ^22.19 || ^24.11 || >=26, as the README badge states). Real-Hopper verification additionally requires either macOS 12+ or an officially supported Linux host (Ubuntu 24.04+, Fedora 41+, 64-bit Arch, or CachyOS) and an installed Hopper application. Linux demo verification uses its own private Xvfb display and does not require a desktop session. Run `nvm use` before installing dependencies.
```bash
npm ci
@@ -120,32 +120,14 @@ Describe the behavior change and verification performed in the pull request. Nev
## Maintainer release checklist
Run `npm run check:pr`, the isolated package verifier, package dry run, and two-target real-Hopper verifier described above. Build a local tarball and exercise the executable through the package boundary:
Use the [checkpoint release guide](docs/releasing.md). Releases start from an
explicit `release/VERSION` branch cut at a recorded commit. Pushing the branch prepares
the bot PR. Wait for its exact-head CI and review; merging into that frozen
branch triggers publication through the official Release workflow. Main
pushes do not update release PRs or publish packages. Both publishers build
the exact SHA tagged by Release Please.
```bash
npm pack
```
Use the exact filename printed by `npm pack` to run the packaged executable:
```bash
npm exec --yes --package ./rea-agents-VERSION.tgz -- rea --help
```
Replace `VERSION` with the packed version; do not use a tarball from an earlier
build.
Publish the public package:
```bash
npm publish --access public
```
After npm registry propagation, verify the published CLI and connect the client SDK version pinned in `package.json` to the published server to confirm the canonical tool catalog:
```bash
npx -y rea-agents@latest --help
npx -y rea-agents@latest doctor
npx -y rea-agents@latest setup --yes --all-detected
npx -y rea-agents@latest mcp
```
Keep new implementation commits on main for the next release. The workflow
owns packaged-artifact verification, npm publication, the published CLI/MCP
canary, and MCP Registry publication. Sync release metadata back to main after
publication; see the guide for partial-publication recovery and verification.
+11 -7
View File
@@ -17,7 +17,7 @@
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
**[Website](https://morluto.github.io/rea/) · [Guides](https://morluto.github.io/rea/guides/) · [DX-Ball showcase](https://morluto.github.io/rea/showcase/dx-ball/)**
**[Website](https://morluto.github.io/rea/) · [Guides](https://morluto.github.io/rea/guides/) · [Showcases](https://morluto.github.io/rea/showcase/)**
[Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [Tool catalog](#tool-catalog-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works)
@@ -157,7 +157,7 @@ external-tool prerequisites depend on the selected workflow; the native
provider guides describe their supported platforms.
- macOS 12 or newer
- Ubuntu 24.04+, Fedora 41+, or 64-bit Arch Linux
- Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, or CachyOS
- Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+
- npm; REA does not require or install a particular npm version
@@ -183,7 +183,7 @@ On macOS, setup can install Hopper in `~/Applications` after approval. It verifi
On supported Linux distributions, setup can install Hopper and its demo-session dependencies through your system package manager. You may see a system authorization prompt. Demo sessions use a private virtual display, leaving your desktop alone. See [Hopper installation](docs/installation.md#hopper) for download verification and platform details.
The normal Linux launcher is `/opt/hopper/bin/Hopper`. If Hopper was installed elsewhere:
REA prefers an executable `/opt/hopper/bin/Hopper` on Linux. If it is unavailable, REA automatically checks `~/.local/share/rea/hopper/bin/Hopper`. If Hopper was installed elsewhere:
```bash
export HOPPER_LAUNCHER_PATH=/absolute/path/to/Hopper
@@ -193,12 +193,12 @@ rea doctor --json
If doctor reports a missing analysis engine even though the file exists, inspect shared-library resolution with:
```bash
ldd /opt/hopper/bin/Hopper | grep 'not found'
ldd /absolute/path/to/Hopper | grep 'not found'
```
Install the missing packages and rerun `rea setup`. The Linux demo needs Xvfb, Python 3, X11, and XTEST; approved setup installs these dependencies. If you use the curl installer, add `~/.local/bin` to your shell `PATH` when needed.
REA defaults `HOPPER_LAUNCHER_PATH` to `/Applications/Hopper Disassembler.app/Contents/MacOS/hopper` on macOS and `/opt/hopper/bin/Hopper` on Linux. Explicit configuration always takes precedence.
REA uses `/Applications/Hopper Disassembler.app/Contents/MacOS/hopper` by default on macOS. On Linux it prefers executable `/opt/hopper/bin/Hopper`, then executable `~/.local/share/rea/hopper/bin/Hopper`, and keeps `/opt/hopper/bin/Hopper` as the diagnostic fallback if neither exists. Explicit `HOPPER_LAUNCHER_PATH` configuration always takes precedence.
### Ghidra analysis provider
@@ -410,7 +410,7 @@ See [native investigation](docs/native-investigation.md) for keyed archives, ins
| Managed PE/CLI | 7 | .NET identity, metadata, CIL instructions, native dependencies, reconstruction imports, and build comparisons |
| Firmware | 2 | Linux firmware region inspection and explicit extraction |
| Android APK | 5 | package and manifest declarations, class search, member inventories, method decompilation, and incoming static references |
| Browser observation | 9 | page structure, network metadata, scripts, source maps, WebMCP discovery, screenshots, and capture comparisons |
| Browser observation | 11 | page structure, network metadata, scripts, source maps, WebMCP discovery, screenshots, and capture comparisons |
| Electron analysis | 5 | renderer observation, static app mapping, and static/runtime reconciliation |
| JavaScript runtime | 2 | Node/Electron Inspector target discovery, script locations, and execution-context events |
| Application workflows | 13 | captured website script export; Android/Apple inventory projections; cross-layer feature traces, build comparisons, historical source mapping, static return-shape comparison, and reconstruction checks |
@@ -434,6 +434,10 @@ Static Android APK inspection supports Linux and macOS; the current metadata bri
and tracing tools. Source URLs, frame or transaction references, competing
versions, and missing-source states remain inline. See
[captured website scripts](docs/website-script-export.md).
Inspect a selected node's listener sources or observe an externally triggered
execution window with `inspect_web_event_listeners` / `observe_web_execution`.
Precise coverage resets counters and affects optimized execution; see
[website runtime attribution](docs/web-runtime.md).
Trace one exported script's native imports with `trace_web_module_imports` /
`rea trace-web-module-imports`, preserving query/fragment identity and optional
import-map context. Requires caller-supplied Chromium via
@@ -549,7 +553,7 @@ Setup offers supported agent integrations for selection. Existing REA registrati
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@4.1.0", "mcp"]
"args": ["-y", "rea-agents@5.0.0", "mcp"]
}
}
}
+5 -5
View File
@@ -138,7 +138,7 @@ rea setup
### المتطلبات
- macOS 12 أو أحدث
- Ubuntu 24.04+ أو Fedora 41+ أو Arch Linux بنواة 64 بت
- Ubuntu 24.04+ أو Fedora 41+ أو Arch Linux بنواة 64 بت أو CachyOS
- Node.js 22.x (>=22.19) أو 24.x (>=24.11) أو 26+ وnpm
يتطلب تحليل الملفات التنفيذية الأصلية Hopper أو Ghidra. Hopper برنامج منفصل، وللوضع التجريبي قيود يحددها المورّد، لكن الترخيص المدفوع ليس إلزاميًا.
@@ -153,7 +153,7 @@ rea setup
يشخّص `npx -y rea-agents@latest doctor` الجهاز والاعتماديات وأدوات التحليل وإعدادات الوكيل من دون تغييرها. أضف `--json` للحصول على تشخيص منظم.
مسار Hopper الافتراضي على Linux هو `/opt/hopper/bin/Hopper`. استخدم `HOPPER_LAUNCHER_PATH` للمسارات الأخرى. إذا كان الملف موجودًا لكن محرك التحليل لا يظهر، فشغّل `ldd /opt/hopper/bin/Hopper | grep 'not found'` للتحقق من المكتبات الناقصة. راجع [دليل Hopper](docs/installation.md#hopper).
على Linux، يفضّل REA الملف التنفيذي `/opt/hopper/bin/Hopper`، وإذا لم يكن متاحًا يتحقق تلقائيًا من `~/.local/share/rea/hopper/bin/Hopper`. استخدم `HOPPER_LAUNCHER_PATH` للمسارات الأخرى. إذا كان الملف موجودًا لكن محرك التحليل لا يظهر، فشغّل `ldd /absolute/path/to/Hopper | grep 'not found'` على مسار Hopper الفعلي للتحقق من المكتبات الناقصة. راجع [دليل Hopper](docs/installation.md#hopper).
### التحديث والإزالة
@@ -222,7 +222,7 @@ rea setup
| PE/CLI المُدار | 7 | هوية تجميعات .NET وبياناتها الوصفية وتعليمات CIL والاعتماديات الأصلية واستيراد إعادة البناء ومقارنة الإصدارات |
| البرامج الثابتة | 2 | فحص مناطق البرامج الثابتة على Linux واستخراجها الصريح |
| Android APK | 5 | بيانات الحزمة وmanifest والبحث عن الأصناف وقوائم الأعضاء وفك ترجمة الأساليب والمراجع الثابتة |
| مراقبة المتصفح | 9 | بنية الصفحات وبيانات الشبكة والبرامج النصية وخرائط المصدر واكتشاف WebMCP والصور ومقارنة الالتقاط |
| مراقبة المتصفح | 11 | بنية الصفحات وبيانات الشبكة والبرامج النصية وخرائط المصدر واكتشاف WebMCP والصور ومقارنة الالتقاط |
| تحليل Electron | 5 | مراقبة العارضين وخريطة التطبيق الثابتة والمواءمة بين نتائج الثبات والتشغيل |
| وقت تشغيل JavaScript | 2 | اكتشاف أهداف Node/Electron Inspector ومواضع البرامج وأحداث سياقات التنفيذ |
| سير عمل التطبيقات | 13 | تصدير النصوص البرمجية الملتقطة، تتبع الميزات عبر الطبقات ومقارنة الإصدارات ومواءمة المصدر التاريخي ومقارنة أشكال القيم المعادة والتحقق من إعادة الإنشاء |
@@ -243,7 +243,7 @@ rea setup
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@4.1.0", "mcp"]
"args": ["-y", "rea-agents@5.0.0", "mcp"]
}
}
}
@@ -313,7 +313,7 @@ npx -y rea-agents@latest compare /absolute/path/to/left-evidence.json /absolute/
<details>
<summary><strong>هل يعمل REA على Linux أو Windows؟</strong></summary>
يدعم REA نظام macOS 12+ وUbuntu 24.04+ وFedora 41+ وArch Linux بنواة 64 بت. يعمل تحليل Ghidra للقراءة فقط على Linux x64 وmacOS x64/arm64 مع Ghidra 12.1.x وJDK الكامل الذي يعلنه التثبيت (JDK 21 أو أحدث دون حد أعلى في إصدارات 12.1 الحالية)، إضافة إلى حد Windows x64 P0 التجريبي لتطبيقات PE الأصلية x86-64 على NTFS محلي؛ راجع [دليل Windows Ghidra P0](docs/windows-ghidra-p0.md) للنطاق الموثق.
يدعم REA نظام macOS 12+ وUbuntu 24.04+ وFedora 41+ وArch Linux بنواة 64 بت وCachyOS. يعمل تحليل Ghidra للقراءة فقط على Linux x64 وmacOS x64/arm64 مع Ghidra 12.1.x وJDK الكامل الذي يعلنه التثبيت (JDK 21 أو أحدث دون حد أعلى في إصدارات 12.1 الحالية)، إضافة إلى حد Windows x64 P0 التجريبي لتطبيقات PE الأصلية x86-64 على NTFS محلي؛ راجع [دليل Windows Ghidra P0](docs/windows-ghidra-p0.md) للنطاق الموثق.
</details>
+4 -4
View File
@@ -138,7 +138,7 @@ rea setup
### 要件
- macOS 12 以降
- Ubuntu 24.04+、Fedora 41+、または 64 ビット Arch Linux
- Ubuntu 24.04+、Fedora 41+、64 ビット Arch Linux、または CachyOS
- Node.js 22.x (>=22.19)、24.x (>=24.11)、または 26+ と npm
ネイティブバイナリ解析には Hopper または Ghidra が必要です。Hopper は別製品です。デモにはベンダー所定の制限がありますが、有料ライセンスは必須ではありません。
@@ -153,7 +153,7 @@ Setup はインストールを確認し、パスを保存できます。Ghidra
`npx -y rea-agents@latest doctor` はホスト、依存関係、解析ツール、エージェント設定を変更せずに確認します。構造化された診断には `--json` を追加してください。
Linux の既定ランチャーは `/opt/hopper/bin/Hopper` です。別の場所には `HOPPER_LAUNCHER_PATH` を設定します。ファイルがあるのに解析エンジンが見つからない場合は、`ldd /opt/hopper/bin/Hopper | grep 'not found'` で不足ライブラリを確認してください。詳しくは [Hopper ガイド](docs/installation.md#hopper)を参照してください。
Linux では実行可能な `/opt/hopper/bin/Hopper` を優先し、利用できない場合は `~/.local/share/rea/hopper/bin/Hopper` を自動的に確認します。それ以外の場所では `HOPPER_LAUNCHER_PATH` を設定します。ファイルがあるのに解析エンジンが見つからない場合は、実際の Hopper パスに対して `ldd /absolute/path/to/Hopper | grep 'not found'` を実行し、不足ライブラリを確認してください。詳しくは [Hopper ガイド](docs/installation.md#hopper)を参照してください。
### 更新とアンインストール
@@ -211,7 +211,7 @@ REA は手順 1〜5 のバイナリ解析を処理し、手順 6 はエージェ
| マネージド PE/CLI | 7 | .NET の識別、メタデータ、CIL 命令、ネイティブ依存関係、再構築インポート、ビルド比較 |
| ファームウェア | 2 | Linux ファームウェア領域の検査と明示的な展開 |
| Android APK | 5 | パッケージと manifest の宣言、クラス検索、メンバー一覧、メソッド逆コンパイル、静的参照 |
| ブラウザー観察 | 9 | ページ構成、ネットワークメタデータ、スクリプト、ソースマップ、WebMCP 検出、スクリーンショット、キャプチャ比較 |
| ブラウザー観察 | 11 | ページ構成、ネットワークメタデータ、スクリプト、ソースマップ、WebMCP 検出、スクリーンショット、キャプチャ比較 |
| Electron 解析 | 5 | レンダラー観察、静的アプリマッピング、静的/実行時結果の対応付け |
| JavaScript 実行時 | 2 | Node/Electron Inspector 対象の検出、スクリプト位置、実行コンテキストイベント |
| アプリワークフロー | 13 | 取得した Web スクリプトのエクスポート、層をまたぐ機能トレース、ビルド比較、履歴ソース対応、静的戻り値形状の比較、再実装検証 |
@@ -232,7 +232,7 @@ Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@4.1.0", "mcp"]
"args": ["-y", "rea-agents@5.0.0", "mcp"]
}
}
}
+4 -4
View File
@@ -139,7 +139,7 @@ rea setup
### 요구 사항
- macOS 12 이상
- Ubuntu 24.04+, Fedora 41+ 또는 64비트 Arch Linux
- Ubuntu 24.04+, Fedora 41+, 64비트 Arch Linux 또는 CachyOS
- Node.js 22.x (>=22.19), 24.x (>=24.11) 또는 26+와 npm
네이티브 바이너리 분석에는 Hopper 또는 Ghidra가 필요합니다. Hopper는 별도 소프트웨어입니다. 데모에는 공급업체의 제한이 있지만 유료 라이선스가 필수는 아닙니다.
@@ -154,7 +154,7 @@ Setup은 설치를 확인하고 경로를 저장할 수 있습니다. Ghidra, Ja
`npx -y rea-agents@latest doctor`는 호스트, 의존성, 분석 도구, 에이전트 설정을 변경 없이 확인합니다. 구조화된 진단에는 `--json`을 추가하세요.
Linux의 기본 Hopper 실행 파일은 `/opt/hopper/bin/Hopper`입니다. 다른 위치에는 `HOPPER_LAUNCHER_PATH`를 설정하세요. 파일이 있는데도 분석 엔진이 없다고 하면 `ldd /opt/hopper/bin/Hopper | grep 'not found'`로 누락된 라이브러리를 확인하세요. 자세한 내용은 [Hopper 안내](docs/installation.md#hopper)를 참고하세요.
Linux에서는 실행 가능한 `/opt/hopper/bin/Hopper`를 우선 사용하고, 사용할 수 없으면 `~/.local/share/rea/hopper/bin/Hopper`를 자동으로 확인합니다. 다른 위치에는 `HOPPER_LAUNCHER_PATH`를 설정하세요. 파일이 있는데도 분석 엔진이 없다고 하면 실제 Hopper 경로에 대해 `ldd /absolute/path/to/Hopper | grep 'not found'`를 실행해 누락된 라이브러리를 확인하세요. 자세한 내용은 [Hopper 안내](docs/installation.md#hopper)를 참고하세요.
### 업데이트와 제거
@@ -212,7 +212,7 @@ REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이
| 관리 PE/CLI | 7 | .NET 식별, 메타데이터, CIL 명령어, 네이티브 종속성, 재구성 가져오기, 빌드 비교 |
| 펌웨어 | 2 | Linux 펌웨어 영역 검사 및 명시적 추출 |
| Android APK | 5 | 패키지와 manifest 선언, 클래스 검색, 멤버 목록, 메서드 디컴파일, 정적 참조 |
| 브라우저 관찰 | 9 | 페이지 구조, 네트워크 메타데이터, 스크립트, 소스 맵, WebMCP 탐색, 스크린샷, 캡처 비교 |
| 브라우저 관찰 | 11 | 페이지 구조, 네트워크 메타데이터, 스크립트, 소스 맵, WebMCP 탐색, 스크린샷, 캡처 비교 |
| Electron 분석 | 5 | 렌더러 관찰, 정적 앱 매핑, 정적·런타임 결과 연결 |
| JavaScript 런타임 | 2 | Node/Electron Inspector 대상 탐색, 스크립트 위치, 실행 컨텍스트 이벤트 |
| 앱 워크플로 | 13 | 캡처한 웹 스크립트 내보내기, 계층 간 기능 추적, 빌드 비교, 히스토리 소스 매핑, 정적 반환 구조 비교, 재구현 검증 |
@@ -233,7 +233,7 @@ Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@4.1.0", "mcp"]
"args": ["-y", "rea-agents@5.0.0", "mcp"]
}
}
}
+4 -4
View File
@@ -138,7 +138,7 @@ rea setup
### 系统要求
- macOS 12 或更高版本
- Ubuntu 24.04+、Fedora 41+ 或 64 位 Arch Linux
- Ubuntu 24.04+、Fedora 41+、64 位 Arch Linux 或 CachyOS
- Node.js 22.x (>=22.19)、24.x (>=24.11) 或 26+,以及 npm
原生二进制分析需要 Hopper 或 Ghidra。Hopper 是独立软件;演示模式有厂商规定的限制,不要求购买许可证。
@@ -153,7 +153,7 @@ Setup 可以验证安装并保存路径,不会安装或升级 Ghidra、Java、
运行 `npx -y rea-agents@latest doctor`,检查主机、依赖、分析工具和智能体配置。该命令不会修改文件。添加 `--json` 可获取结构化诊断。
Linux 上的默认 Hopper 启动器为 `/opt/hopper/bin/Hopper`。其他路径可通过 `HOPPER_LAUNCHER_PATH` 指定。如果文件存在但仍报告缺少分析引擎,运行 `ldd /opt/hopper/bin/Hopper | grep 'not found'` 检查缺少的系统库。安装详情见 [Hopper 指南](docs/installation.md#hopper)。
Linux 上 REA 会优先使用可执行的 `/opt/hopper/bin/Hopper`;如果不可用,则自动检查 `~/.local/share/rea/hopper/bin/Hopper`。其他路径可通过 `HOPPER_LAUNCHER_PATH` 指定。如果文件存在但仍报告缺少分析引擎,请对实际 Hopper 路径运行 `ldd /absolute/path/to/Hopper | grep 'not found'` 检查缺少的系统库。安装详情见 [Hopper 指南](docs/installation.md#hopper)。
### 更新与卸载
@@ -211,7 +211,7 @@ REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常
| 托管 PE/CLI | 7 | .NET 程序集身份、元数据、CIL 指令、原生依赖声明、重建导入与构建比较 |
| 固件 | 2 | Linux 固件区域检查与显式提取 |
| Android APK | 5 | 包与 manifest 声明、类搜索、成员清单、方法反编译与静态引用 |
| 浏览器观察 | 9 | 页面结构、网络元数据、脚本、来源映射、WebMCP 发现、截图与捕获比较 |
| 浏览器观察 | 11 | 页面结构、网络元数据、脚本、来源映射、WebMCP 发现、截图与捕获比较 |
| Electron 分析 | 5 | 渲染进程观察、静态应用映射、静态/运行时结果关联 |
| JavaScript 运行时 | 2 | Node/Electron Inspector 目标发现、脚本位置与执行上下文事件 |
| 应用工作流 | 13 | 捕获的网站脚本导出、跨层功能追踪、构建比较、历史源码映射、静态返回结构比较与重建验证 |
@@ -232,7 +232,7 @@ Setup 支持 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsu
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@4.1.0", "mcp"]
"args": ["-y", "rea-agents@5.0.0", "mcp"]
}
}
}
+16
View File
@@ -1,8 +1,24 @@
import Foundation
struct ChildBatch<Element> {
let values: [Element]
let complete: Bool
}
struct ChildCount {
let value: Int?
let error: Int32?
}
func serializeHelperOutput(_ value: Any) throws -> Data {
try JSONSerialization.data(withJSONObject: value, options: [.sortedKeys, .withoutEscapingSlashes])
}
func captureChildCount(status: Int32, success: Int32, value: Int) -> ChildCount {
guard status == success else { return ChildCount(value: nil, error: status) }
return ChildCount(value: value, error: nil)
}
func captureChildValues<Element>(requestedCount: Int, copy: () -> [Element]?) -> ChildBatch<Element> {
guard requestedCount > 0 else { return ChildBatch(values: [], complete: true) }
let returned = copy() ?? []
+19 -9
View File
@@ -22,9 +22,10 @@ func attribute(_ element: AXUIElement, _ key: String) -> CFTypeRef? {
var value: CFTypeRef?
return AXUIElementCopyAttributeValue(element, key as CFString, &value) == .success ? value : nil
}
func childCount(_ element: AXUIElement) -> Int {
func childCount(_ element: AXUIElement) -> ChildCount {
var count = 0
return AXUIElementGetAttributeValueCount(element, kAXChildrenAttribute as CFString, &count) == .success ? count : 0
let status = AXUIElementGetAttributeValueCount(element, kAXChildrenAttribute as CFString, &count)
return captureChildCount(status: status.rawValue, success: AXError.success.rawValue, value: count)
}
func children(_ element: AXUIElement, count: Int) -> ChildBatch<AXUIElement> {
captureChildValues(requestedCount: count) {
@@ -88,7 +89,11 @@ func observe(_ request: Request) async throws -> [String: Any] {
var element = root
for index in action.path ?? [] {
var selectedChild: CFArray?
guard index >= 0 && index < childCount(element),
let count = childCount(element)
guard let available = count.value else {
try fail("element-count-unavailable", "Cannot validate selected accessibility path because child count failed with AXError \(count.error ?? -1)")
}
guard index >= 0 && index < available,
AXUIElementCopyAttributeValues(element, kAXChildrenAttribute as CFString, index, 1, &selectedChild) == .success,
let item = (selectedChild as? [AXUIElement])?.first else { try fail("element-missing", "Selected accessibility path no longer exists") }
element = item
@@ -104,15 +109,20 @@ func observe(_ request: Request) async throws -> [String: Any] {
}
guard outcome == .success else { try fail("action-failed", "Accessibility action failed with AXError \(outcome.rawValue)") }
}
var nodes: [[String: Any]] = []; var truncated = false
var nodes: [[String: Any]] = []; var truncated = false; var gaps: [String] = []
if request.accessibility, let root = selected {
var pending: [(AXUIElement, [Int])] = [(root, [])]
while let (element, path) = pending.popLast() {
if nodes.count >= request.max_nodes { truncated = true; break }
let totalChildren = childCount(element)
let childCountResult = childCount(element)
var actions: CFArray?
AXUIElementCopyActionNames(element, &actions)
nodes.append(["path": path, "role": text(element, kAXRoleAttribute), "title": text(element, kAXTitleAttribute), "value": text(element, kAXValueAttribute), "actions": actions as? [String] ?? [], "children_count": totalChildren])
nodes.append(["path": path, "role": text(element, kAXRoleAttribute), "title": text(element, kAXTitleAttribute), "value": text(element, kAXValueAttribute), "actions": actions as? [String] ?? [], "children_count": childCountResult.value.map { $0 as Any } ?? NSNull()])
guard let totalChildren = childCountResult.value else {
truncated = true
gaps.append("AX child count unavailable at path \(path): AXError \(childCountResult.error ?? -1)")
continue
}
if path.count >= 32 { if totalChildren > 0 { truncated = true }; continue }
let available = max(0, request.max_nodes - nodes.count - pending.count)
let count = min(available, totalChildren)
@@ -143,18 +153,18 @@ func observe(_ request: Request) async throws -> [String: Any] {
}
screenshot = ["mime_type": "image/png", "base64": png.base64EncodedString(), "sha256": SHA256.hash(data: png).map({ String(format: "%02x", $0) }).joined(), "width": image.width, "height": image.height]
}
return ["window": ["pid": request.pid, "window_id": request.window_id, "executable": request.executable, "launch_time": launch, "title": window[kCGWindowName as String] as? String ?? ""], "nodes": nodes, "truncated": truncated, "screenshot": screenshot, "gaps": request.accessibility ? [] : ["Accessibility capture was disabled"]]
return ["window": ["pid": request.pid, "window_id": request.window_id, "executable": request.executable, "launch_time": launch, "title": window[kCGWindowName as String] as? String ?? ""], "nodes": nodes, "truncated": truncated, "screenshot": screenshot, "gaps": request.accessibility ? gaps : ["Accessibility capture was disabled"]]
}
Task { @MainActor in
do {
let request = try JSONDecoder().decode(Request.self, from: Data(CommandLine.arguments[1].utf8))
let result = try await observe(request)
let output = try JSONSerialization.data(withJSONObject: ["ok": true, "result": result], options: [.sortedKeys])
let output = try serializeHelperOutput(["ok": true, "result": result])
FileHandle.standardOutput.write(output)
} catch {
let failure = error as? BoundaryFailure
let output: [String: Any] = ["ok": false, "code": failure?.code ?? "capture-failed", "message": failure?.message ?? error.localizedDescription]
if let data = try? JSONSerialization.data(withJSONObject: output, options: [.sortedKeys]) { FileHandle.standardOutput.write(data) }
if let data = try? serializeHelperOutput(output) { FileHandle.standardOutput.write(data) }
}
exit(0)
}
+1
View File
@@ -64,6 +64,7 @@ export default defineConfig({
{ text: "MCP contracts", link: "/mcp-contracts" },
{ text: "Tool design", link: "/tool-design" },
{ text: "Testing", link: "/testing" },
{ text: "Releasing", link: "/releasing" },
{ text: "Architecture decisions", link: "/adr/README" },
],
},
@@ -30,7 +30,7 @@ analyzer, or provider-version differences while appearing target-compatible.
The decision addressed the earlier representations of
[`CompositeProvider`](https://github.com/morluto/rea/blob/main/src/application/binary/CompositeProvider.ts),
[runtime composition](https://github.com/morluto/rea/blob/main/src/application/runtime.ts),
[runtime composition](https://github.com/morluto/rea/blob/main/src/composition/binary.ts),
[`BinarySession`](https://github.com/morluto/rea/blob/main/src/application/binary/BinarySession.ts),
[`BinaryTarget`](https://github.com/morluto/rea/blob/main/src/domain/binaryTarget.ts),
[analysis snapshot](https://github.com/morluto/rea/blob/main/src/domain/analysisSnapshot.ts), and
+14 -5
View File
@@ -5,6 +5,10 @@ graph TD
DISPATCH["scripts/rea.mjs<br/>Executable dispatcher<br/>routes mcp / one-shot"]
end
subgraph "Production Composition"
COMPOSITION["src/composition/<br/>fresh typed provider and session factories<br/>one-shot binding and independent optional loading"]
end
subgraph "Adapters"
SERVER["src/server/<br/>MCP request translation<br/>createServer, tool registration, toolResult"]
HOPPER_BRIDGE["bridge/hopper_bridge.py<br/>Runs inside Hopper<br/>adapts operations to Hopper Python API"]
@@ -18,7 +22,7 @@ graph TD
REGISTRY["AnalysisProviderRegistry<br/>sorted deep candidates<br/>deterministic selection, no fallback"]
RECORDS["src/application/investigation/<br/>composed InvestigationRecords + narrow ports<br/>EvidenceLedger and Unknown ownership"]
SNAPSHOT["BinarySessionRecords + AnalysisSnapshotCache<br/>binary target/profile snapshots<br/>compatibility facade and post-commit notifications"]
JS_RECON["src/application/ + src/artifacts/<br/>JavaScript artifact reconstruction<br/>directory/ASAR reads<br/>AST-only bundle projection"]
JS_RECON["src/application/javascript/ + src/artifacts/javascript/<br/>JavaScript artifact reconstruction<br/>directory/ASAR reads<br/>AST-only bundle projection"]
end
subgraph "Provider Layers"
@@ -27,7 +31,7 @@ graph TD
IDA["src/ida/<br/>Upstream MCP contract adaptation<br/>live GUI binding or owned headless database"]
IDA_MCP["External ida-pro-mcp<br/>GUI proxy or database supervisor<br/>upstream installation and engine lifecycle"]
AUX["src/artifacts/ + src/native/<br/>disjoint artifact and native capabilities"]
BROWSER["src/browser/<br/>CDP / Playwright capture<br/>captured scripts and native module resolution"]
BROWSER["src/browser/<br/>CDP / Playwright capture<br/>captured scripts and native module resolution<br/>execution/: coverage, listener and source attribution"]
JS_CODEC["src/javascript/sourceMaps/<br/>pinned upstream map codec<br/>owned heap-limited Node process, complete point evidence"]
INSPECTOR["src/inspector/<br/>passive V8 Inspector discovery and capture<br/>script and target locations"]
DOTNET["src/dotnet/<br/>Managed static metadata provider<br/>PE/CLI metadata inventory, members, IL"]
@@ -69,6 +73,10 @@ graph TD
MAIN --> SERVER
MAIN --> APP
CLI --> APP
MAIN --> COMPOSITION
CLI --> COMPOSITION
SERVER --> COMPOSITION
COMPOSITION --> SESSION
APP --> FIRMWARE
FIRMWARE --> PROCESS
FIRMWARE --> CONTRACTS
@@ -84,9 +92,10 @@ graph TD
APP -->|record callbacks| RECORDS
SERVER -->|narrow record ports| RECORDS
RECORDS --> DOMAIN
REGISTRY --> HOPPER
REGISTRY --> GHIDRA
REGISTRY --> IDA
COMPOSITION --> HOPPER
COMPOSITION --> GHIDRA
COMPOSITION --> IDA
COMPOSITION --> AUX
IDA --> IDA_MCP
IDA --> CONTRACTS
IDA --> DOMAIN
+88 -19
View File
@@ -6,17 +6,17 @@ there is no new universal provider interface or fixed module manifest.
## Ownership to preserve
| Responsibility | Current owner | Migration direction |
| --------------------------------------------- | ---------------------------------------- | -------------------------------------------------------------- |
| Analyst semantics and Evidence representation | domain | Keep pure and provider-neutral |
| Named public input/output contracts | contracts | Keep exact CLI/MCP meaning and complete discovery |
| Producer parsing and external tool protocols | provider adapters | Keep behind the relevant typed port |
| Shared analyst workflows | application | Share between CLI and MCP |
| Concrete provider construction | binary runtime, composition, MCP startup | Consolidate one capability at a time in production composition |
| Investigation Evidence and Unknowns | InvestigationRecords/EvidenceLedger | Consume narrow read/write/atomic record ports |
| Target/profile snapshots and invalidation | BinarySessionRecords/BinarySession | Keep binary-owned |
| Subprocess ownership and host primitives | process/windows | Reuse; preserve cancellation and cleanup |
| Public translation | CLI/MCP adapters | Delegate to shared workflows and named contracts |
| Responsibility | Current owner | Migration direction |
| --------------------------------------------- | ----------------------------------- | ----------------------------------------------------- |
| Analyst semantics and Evidence representation | domain | Keep pure and provider-neutral |
| Named public input/output contracts | contracts | Keep exact CLI/MCP meaning and complete discovery |
| Producer parsing and external tool protocols | provider adapters | Keep behind the relevant typed port |
| Shared analyst workflows | application | Share between CLI and MCP |
| Concrete provider construction | composition, MCP startup | Keep fresh typed factories at the production boundary |
| Investigation Evidence and Unknowns | InvestigationRecords/EvidenceLedger | Consume narrow read/write/atomic record ports |
| Target/profile snapshots and invalidation | BinarySessionRecords/BinarySession | Keep binary-owned |
| Subprocess ownership and host primitives | process/windows | Reuse; preserve cancellation and cleanup |
| Public translation | CLI/MCP adapters | Delegate to shared workflows and named contracts |
Retain exact Evidence IDs, provenance, detached reads, atomic Evidence/Unknown
mutations and optimistic revisions. Preserve current close and failed-cleanup
@@ -69,8 +69,8 @@ paths removed during a move. Pass exact source paths for a focused check.
Pure source and its tests keep inward dependencies. Application/server provider
construction guards cover the migrated deep, Android, firmware, JavaScript recovery and observation
implementations. The existing binary runtime is an exact temporary composition
exception; shared browser capture/export helpers remain admitted while their
implementations. Application workflows cannot import composition, including the
former binary runtime entrypoint; shared browser capture/export helpers remain admitted while their
ownership is reviewed. Test lane restrictions still use Oxlint. Producer-dependent
browser and managed comparison tests live in boundary lanes with their original
fixtures and assertions. This is a development guard, not runtime authorization.
@@ -104,8 +104,8 @@ inside the provider. The existing launcher seams remain available to boundary
fixtures, which now exercise the production factories.
Application workflows and MCP registration must not import these concrete
providers. Composition may import their implementations. Binary composition is
still at its existing entrypoint while its direct-analysis callers are investigated.
providers. Composition may import their implementations. Binary composition also
uses this outer boundary after its direct-analysis callers were migrated.
This concrete pilot does not introduce a shared provider interface or module
manifest.
@@ -329,10 +329,9 @@ records still have their separate owner in `src/application/investigation/`.
`AnalysisProvider.ts` stays shared because its identity/execution types also serve
nonbinary ports. `BinaryTargetResolver.ts` stays shared because Android, firmware,
managed and artifact workflows use its target parsing. The existing `runtime.ts`
remains the exact temporary production wiring entrypoint while direct-analysis
callers are migrated deliberately; application workflows gain no outward
composition dependency.
managed and artifact workflows use its target parsing. Production wiring lives
in `src/composition/binary.ts`; application workflows gain no outward composition
dependency.
The three colocated application tests move with their owner. Composition cases
remain in `tests/composition/analysis-sessions/`; filesystem and SDK cases retain
@@ -356,3 +355,73 @@ repository-root default. Both point to `_reference/firmware-integration/generate
when invoked through the documented npm commands. Optional ext4/Ghidra lanes keep
their own prerequisites. Source-fixture generation and synthetic producer tests
do not establish real Binwalk/Unblob analysis or another host's execution support.
## Apple artifact capability ownership
`src/domain/apple/` owns Apple application anatomy/projection, asset catalog
facts, Interface Builder graphs, keyed archive semantics and plist values.
`src/application/apple/AppleApplicationService.ts` retains the shared workflow
that projects authenticated IPA/macOS inventory Evidence.
`src/artifacts/apple/` owns the actual producer boundary: directory acquisition,
assetutil invocation, plist/NIB decoding, encoded view-parent interpretation and
keyed archive inspection. These helpers are used by ArtifactProvider, retain
its existing typed analysis boundary, injected assetutil seam and original reader
cleanup, and do not require a new port for each helper. Producer code no longer
lives in the application layer. Guards reject application/composition/caller
imports from this owner while admitting pure facts and shared artifact readers.
Adapter-source tests retain their assertions in the existing forked adapter lane.
`scripts/verify/apple/` groups archive, asset catalog, Interface Builder,
Objective-C/Swift dispatch and macOS bundle verification. The macOS bundle fixture
builder lives in `scripts/fixtures/apple/`; NIB byte fixtures stay beside their
artifact decoder. Existing format-specific conformance sources and goldens keep
their locations, and relocated verifiers resolve them against their own file
URLs. Public npm entrypoints and CLI/MCP contracts stay stable; generated-document
cache inputs include the relocated runtime scripts and fixture builder.
Portable decoder and injected producer tests establish their format/boundary
claims. Actual Swift/Xcode, assetutil, native NIB, bundle signing and DMG lifecycle
claims require the real Apple lane. Native binary API/value-flow semantics and
shared cross-domain contract aggregates keep their separate owners.
## Binary production composition closure
`src/composition/binary.ts` is the shared production owner for deep candidate
construction, lazy auxiliary composition and the managed-only session factory.
It preserves the existing constructor bodies, selection policy and fresh session
per invocation. Factory registration acquires no engine or target.
`DirectAnalysis` and `DirectAnalysisStatus` receive these two existing factories
through `DirectAnalysisDependencies`. They retain configuration parsing, selected
status environment, cancellation, snapshot replay/binding, Evidence projection
and finally-close. The dependency uses the existing BinarySession type, including
its replay policy; it adds no universal backend or lifecycle contract.
`src/composition/directAnalysis.ts` binds the production factories for CLI and
one-shot managed MCP callers. Binding creates no session and stores no retained
analysis state. MCP startup constructs its connection session through the same
binary factory. Source and compiled fixtures use these entrypoints. The former
application runtime path is removed, and the source guard no longer admits its
outward provider/composition imports.
## Native analyst capability ownership
`src/domain/native/` owns portable native inspection, instruction/data types,
load-image facts, API boundaries, annotations, metadata recovery, UI observation
semantics and value/investigation graphs. `src/application/native/` owns shared
API projection, call routes, dispatch inspection and UI/value tracing. Its tests
keep the existing service lane. `src/contracts/native/` owns the named native
adapter contracts; shared official/enhanced aggregates remain at their owners.
Producer interpretation, external commands and host UI lifecycle remain in
`src/native/`, Hopper, Ghidra and IDA. The shared function dossier still lives in
`domain/hopperValues.ts`: its historical name does not make its meaning exclusive
to Hopper. This move retains existing typed ports, exact result schemas,
observed/derived/unknown distinctions, profiles and Evidence links.
Source and compiled verifier imports follow the new owners. Host UI and real
native-value verifier locations are retained until their own proof lanes migrate;
interactive macOS UI success and real dependency tracing require their respective
host/engine workflows. Portable source fixtures and injected call-route tests
establish their narrower boundaries, without expanding provider/platform support.
+1 -1
View File
@@ -408,7 +408,7 @@ For a client that requires manual configuration, use:
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@4.1.0", "mcp"]
"args": ["-y", "rea-agents@5.0.0", "mcp"]
}
}
}
+15 -9
View File
@@ -1,7 +1,7 @@
{
"package": {
"name": "rea-agents",
"version": "4.1.0",
"version": "5.0.0",
"sdk": {
"server": "2.3.1",
"client": "2.3.1",
@@ -10,7 +10,7 @@
"skill_version": "27"
},
"tools": {
"total": 131,
"total": 133,
"families": [
{
"id": "direct",
@@ -142,7 +142,7 @@
{
"id": "browser",
"surface": "browser-provider",
"count": 9,
"count": 11,
"tools": [
"analyze_web_bundle",
"capture_browser_scenario",
@@ -150,8 +150,10 @@
"compare_web_captures",
"compare_web_screenshots",
"discover_webmcp_tools",
"inspect_web_event_listeners",
"inspect_web_page",
"list_browser_targets",
"observe_web_execution",
"observe_web_session"
]
},
@@ -389,8 +391,10 @@
"compare_web_captures",
"compare_web_screenshots",
"discover_webmcp_tools",
"inspect_web_event_listeners",
"inspect_web_page",
"list_browser_targets",
"observe_web_execution",
"observe_web_session"
]
},
@@ -566,7 +570,7 @@
}
],
"cli": {
"primary_count": 86,
"primary_count": 88,
"commands": [
"address-to-file-offset",
"analyze",
@@ -622,6 +626,7 @@
"inspect-native-load-image",
"inspect-plist",
"inspect-signature",
"inspect-web-event-listeners",
"inspect-web-page",
"instructions",
"list-architectures",
@@ -630,6 +635,7 @@
"list-javascript-runtime-targets",
"observe-javascript-runtime",
"observe-native-ui",
"observe-web-execution",
"observe-web-session",
"project-android-application-graph",
"project-apple-application-graph",
@@ -664,15 +670,15 @@
},
"runtime_catalog": {
"counts": {
"cli_commands": 86,
"mcp_tools": 131,
"cli_commands": 88,
"mcp_tools": 133,
"mcp_prompts": 6
},
"digests": {
"tools_sha256": "f49e05c52c6fd9955edcede27fbaaa248fdb917caa7b27edb8d97f7a30ad250e",
"tools_sha256": "af7fe81ec689c2a77f04579b6f4beb4abe86aa71d49c4844d8e0f4d7ed7a3a1e",
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
"combined_sha256": "54ac160db3b96580a9a0203a07fe3db01ffc05224db621f48b4abdf979e4f50b",
"providers_sha256": "a7d885531ee25d42c51385444faec544d34cd7eba6250a7dc75c4faedd4454a3"
"combined_sha256": "83b4c3445ed0ec003c0aeefa3188f123d9ae319484f8b1e2b611d2fe278c9c72",
"providers_sha256": "aea15f517c822d80918ccaba57a4fa06557f484cf27b8acbaed4e1a660fac896"
}
}
}
+103
View File
@@ -0,0 +1,103 @@
# Releasing from a checkpoint
REA releases use a frozen source checkpoint. Main can continue accepting
changes while a release is tested and published. The Release workflow runs
on `release/*` branches; main pushes do not refresh release PRs.
## 1. Cut the release branch
Choose the next version from the unreleased Conventional Commits, including
breaking changes. Record the full source SHA and create `release/VERSION` at
that commit. The version is a maintainer choice; Release Please's proposed
version must agree before publication.
For example, after selecting a reviewed commit for 5.0.0:
```bash
git fetch origin main
git branch release/5.0.0 SOURCE_SHA
git push origin release/5.0.0
```
Replace `SOURCE_SHA` with the selected full commit SHA. The checkpoint must
include this release workflow and CI support for release branches. For an
older checkpoint, backport only the release infrastructure first and record
that additional commit. Do not merge later implementation changes into the
release branch or rebase the candidate onto a moving main.
The branch push runs Release Please against that branch and opens a PR for
the version, changelog, and registry metadata. The generation step normalizes
the product catalog. To retry preparation after a failed run, select the same
frozen branch explicitly:
```bash
gh workflow run release.yml --ref release/5.0.0
```
## 2. Review and merge the bot PR
Record the final bot PR head after normalization. Approve GitHub-blocked bot
workflow runs for that head when needed. Review the candidate's version,
notes, generated metadata, and package contents. Wait for the candidate's CI
and relevant real-provider checks. Routine local iterations need focused
checks; CI owns full deterministic coverage and platform lanes. If an artifact
or real-provider check is unavailable, report that limit before deciding to
publish.
Merge the reviewed PR into `release/5.0.0` with its head SHA matched. **This
merge authorizes and triggers publication.** Further main commits do not change
this candidate. Do not push or merge anything else into the release branch
while publication is running. A necessary release fix must be reviewed and
tested before the bot PR merges and establishes a new recorded checkpoint.
## 3. Verify publication
The merge push runs Release Please again, creates the tag and GitHub release,
and publishes npm followed by MCP Registry metadata. Both publishers check
out Release Please's exact release SHA. The triggering workflow also runs
from that release commit so npm's provenance records the actual source.
Before invoking Release Please, the workflow checks that the release branch
still equals the triggering SHA and rejects main or tag refs. Before either
registry publish, it rejects a mismatch between the tagged release SHA and
the triggering SHA. These checks detect moved branches; maintainers must also
keep the release branch frozen throughout publication.
See [npm's provenance implementation](https://github.com/npm/cli/blob/v11.16.0/workspaces/libnpmpublish/lib/provenance.js)
for the use of GitHub's workflow ref and commit SHA.
The workflow builds the bundled Windows controls and verifies the packaged
artifact before npm publication. It then verifies the published CLI, the
capability-scoped MCP catalog, and the isolated package update path before
publishing MCP Registry metadata.
Record these outcomes separately:
- GitHub release and tag, including the resolved commit SHA.
- npm's exact version and integrity, with the published-package canary passed.
- MCP Registry's exact server version and matching npm package version.
A GitHub tag alone does not establish npm or MCP Registry publication.
## Sync metadata back to main
After publication, open a PR from the release branch back to main. Preserve
main's later implementation changes and resolve generated-file conflicts by
regenerating from the combined contracts with the released package version.
Review and test this synchronization PR, then use a merge commit so the release
tag remains in main's ancestry. Keep the released tag unchanged.
Close any superseded rolling release PR. Future releases repeat the checkpoint
procedure from main; never resume automatic release-PR refreshes on main pushes.
## Partial publication and retries
Inspect the failed job and public registry state before retrying. When npm is
already published, the npm job verifies that exact version instead of
publishing it again. Re-run failed jobs in the original publication run so its
release SHA and outputs stay fixed. If only MCP publication failed, retry that
job after checking that the npm canary succeeded.
Do not start a fresh workflow run to repair an already-created release:
Release Please will not create the same release again. Do not move the tag,
delete the release, or unpublish npm as a retry. A defective public package
requires a reviewed correction and a new version.
+30
View File
@@ -89,6 +89,20 @@ then checks that `inspect-artifact` plus `project-apple-application-graph`
report the same bundle anatomy for all three through the CLI, with stdio MCP
parity. It also checks that the DMG is detached afterwards. It runs in macOS CI.
Apple artifact verifiers live in `scripts/verify/apple/`, with the macOS bundle
builder under `scripts/fixtures/apple/` and NIB byte fixtures beside the decoder
in `src/artifacts/apple/`. The npm entrypoints are unchanged. Format-specific
Swift/XIB/asset sources and goldens retain their locations; real Apple workflows
resolve them from the verifier file URL and run in the macOS CI lane.
Portable native semantics and their tests live in `src/domain/native/`; shared
analyst workflows and service-lane tests live in `src/application/native/`.
Named native contracts live in `src/contracts/native/`. Provider protocol and
host UI tests retain their adapter/boundary lanes, and real verifier command
names remain unchanged.
The existing Apple CI job also runs the host Swift-demangling CLI/MCP regression
suites, including option-like symbols, carriage returns and multiline rejection.
MCP SDK transport tests with recording providers remain integration tests.
They are useful for schema drift and failure projection but do not prove that
Hopper, Ghidra or another substituted engine works. `verify:package` proves
@@ -537,3 +551,19 @@ checks an installed package after building the verifier dependencies. The separa
conditional `real-web-source-map` CI job supplies Chrome and an isolated pinned
fixture compiler; static/unit checks do not acquire a browser. See
[the source location guide](web-source-location.md) for the verified decoder profile.
### Website runtime attribution lane
`npm run verify:browser:runtime` uses caller-supplied
`REA_BROWSER_EXECUTABLE` and an owned synthetic site/profile. It exercises public
CLI and stdio MCP for precise execution and native listener source locations,
including actual armed progress, Unicode/CRLF digests, repeated source URLs with
distinct script IDs, zero branches and function-only unknowns on repeated
coverage, request initiators and an externally owned page that remains open.
An optional entrypoint argument to `scripts/verify-browser-runtime.mjs` runs the
same checks through an isolated installed package. The conditional
`real-web-runtime` CI job runs only for relevant changes and needs no fixture
compiler. Ordinary unit/static gates acquire no browser. See
[website runtime attribution](web-runtime.md) for effects, resource bounds and
coverage limits.
+2 -2
View File
@@ -56,7 +56,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
46,src/application/Uninstall.ts:removeManagedPath symlink,Managed cache/state/skill path is a symbolic link,Refused to follow symlink {path}.,Exposes local path and does not explain manual safety check,"REA did not remove this item because its managed path is a symbolic link. Verify the link target before removing it manually.",implemented,focused assertion passed
47,src/application/Uninstall.ts:removeManagedPath failure,Managed item removal fails,{path} could not be removed.,Exposes local path and gives no recovery,"This item could not be removed. Check file permissions, then rerun uninstall.",implemented,verified by injected managed-path removal failure test
48,src/application/DoctorDiagnostics.ts:node,Runtime version is outside supported ranges,"Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact dependency recovery,Keep current copy,unchanged,verified by doctor unit test
49,src/application/Doctor.ts:host,Host OS/distribution is unsupported,"REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux.",Low risk; clearly states supported choices,Keep current copy,unchanged,verified by doctor unit tests
49,src/application/Doctor.ts:host,Host OS/distribution is unsupported,"REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and CachyOS.",Low risk; clearly states supported choices,Keep current copy,unchanged,verified by doctor unit tests
50,src/application/Doctor.ts:analysis engine,Optional deep-analysis executable cannot be found,"Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH.",Low risk in setup-specific diagnostic; exact recovery paths,Keep current copy,unchanged,verified by doctor unit tests
51,src/application/Doctor.ts:target,Optional target path fails shared validation,Supply a readable local app or program path.,Low risk; tells caller valid target class,Keep current copy,unchanged,verified by doctor unit test
52,src/application/Setup.ts:approval,Setup plan lacks approval,"Review the setup plan, then rerun interactively or with --yes.",Low risk; exact authorization recovery,Keep current copy,unchanged,verified by setup unit tests
@@ -67,7 +67,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
57,src/application/Setup.ts:unsupported platform,Platform is neither macOS nor Linux,REA supports Hopper on macOS and selected 64-bit Linux distributions.,Low risk; states support boundary,Keep current copy,unchanged,verified by setup unit test
58,src/application/SetupHost.ts:unsupported Node,Runtime version cannot run setup,"Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ and rerun setup.",Low risk; exact dependency and retry,Keep current copy,unchanged,verified by setup host tests
59,src/application/Setup.ts:unsupported macOS,macOS version is missing or older than 12,Upgrade to macOS 12 or newer.,Low risk; exact support floor,Keep current copy,unchanged,verified by setup host tests
60,src/application/Setup.ts:unsupported Linux,Linux distribution is outside supported set,"REA supports Hopper on Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux.",Low risk; exact supported alternatives,Keep current copy,unchanged,verified by setup unit test
60,src/application/Setup.ts:unsupported Linux,Linux distribution is outside supported set,"REA supports Hopper on Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and CachyOS.",Low risk; exact supported alternatives,Keep current copy,unchanged,verified by setup unit test
61,src/application/ProcessCli.ts:readJson read,Process scenario or evidence file cannot be read,Raw filesystem exception through CLI framework,Can expose local paths/codes and no recovery,"Process input file could not be read. Check that the path exists and is readable.",implemented,verified by focused CLI test
62,src/application/ProcessCli.ts:readJson size,Process scenario or evidence file exceeds 64 MiB,Process JSON input exceeds byte limit,Does not state limit or recovery,"Process input file is too large. Reduce it below 64 MiB, then try again.",implemented,verified by focused sparse-file CLI test
63,src/application/ProcessCli.ts:readJson parse,Process scenario or evidence file is malformed JSON,Raw JSON parser exception through CLI framework,Can expose parser implementation and lacks recovery,"Process input file is not valid JSON. Repair the file, then try again.",implemented,verified by focused CLI test
1 harm_rank location trigger current_copy user_risk proposed_replacement implementation_status verification_result
56 46 src/application/Uninstall.ts:removeManagedPath symlink Managed cache/state/skill path is a symbolic link Refused to follow symlink {path}. Exposes local path and does not explain manual safety check REA did not remove this item because its managed path is a symbolic link. Verify the link target before removing it manually. implemented focused assertion passed
57 47 src/application/Uninstall.ts:removeManagedPath failure Managed item removal fails {path} could not be removed. Exposes local path and gives no recovery This item could not be removed. Check file permissions, then rerun uninstall. implemented verified by injected managed-path removal failure test
58 48 src/application/DoctorDiagnostics.ts:node Runtime version is outside supported ranges Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Low risk; exact dependency recovery Keep current copy unchanged verified by doctor unit test
59 49 src/application/Doctor.ts:host Host OS/distribution is unsupported REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux. REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and CachyOS. Low risk; clearly states supported choices Keep current copy unchanged verified by doctor unit tests
60 50 src/application/Doctor.ts:analysis engine Optional deep-analysis executable cannot be found Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH. Low risk in setup-specific diagnostic; exact recovery paths Keep current copy unchanged verified by doctor unit tests
61 51 src/application/Doctor.ts:target Optional target path fails shared validation Supply a readable local app or program path. Low risk; tells caller valid target class Keep current copy unchanged verified by doctor unit test
62 52 src/application/Setup.ts:approval Setup plan lacks approval Review the setup plan, then rerun interactively or with --yes. Low risk; exact authorization recovery Keep current copy unchanged verified by setup unit tests
67 57 src/application/Setup.ts:unsupported platform Platform is neither macOS nor Linux REA supports Hopper on macOS and selected 64-bit Linux distributions. Low risk; states support boundary Keep current copy unchanged verified by setup unit test
68 58 src/application/SetupHost.ts:unsupported Node Runtime version cannot run setup Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ and rerun setup. Low risk; exact dependency and retry Keep current copy unchanged verified by setup host tests
69 59 src/application/Setup.ts:unsupported macOS macOS version is missing or older than 12 Upgrade to macOS 12 or newer. Low risk; exact support floor Keep current copy unchanged verified by setup host tests
70 60 src/application/Setup.ts:unsupported Linux Linux distribution is outside supported set REA supports Hopper on Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux. REA supports Hopper on Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and CachyOS. Low risk; exact supported alternatives Keep current copy unchanged verified by setup unit test
71 61 src/application/ProcessCli.ts:readJson read Process scenario or evidence file cannot be read Raw filesystem exception through CLI framework Can expose local paths/codes and no recovery Process input file could not be read. Check that the path exists and is readable. implemented verified by focused CLI test
72 62 src/application/ProcessCli.ts:readJson size Process scenario or evidence file exceeds 64 MiB Process JSON input exceeds byte limit Does not state limit or recovery Process input file is too large. Reduce it below 64 MiB, then try again. implemented verified by focused sparse-file CLI test
73 63 src/application/ProcessCli.ts:readJson parse Process scenario or evidence file is malformed JSON Raw JSON parser exception through CLI framework Can expose parser implementation and lacks recovery Process input file is not valid JSON. Repair the file, then try again. implemented verified by focused CLI test
@@ -1,5 +1,5 @@
{
"manifest_id": "ecm_e7e60d5d08bfe335c4c0b38ab4598eb634444b835202e325241749d152808c94",
"manifest_id": "ecm_880356f1ceceb9e29b43e3e89f7a326aa83132e1e1fae0573245848ba714f404",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
@@ -13,7 +13,7 @@
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "265556bd52f52ad829187fe71831cf05329248286c86e3102f0cdbf8841e1d6d"
"sha256": "97dc167b6e0d41ac69953501c96a918319fa6e4b3be0affe2fc2119294e7101d"
}
],
"claims": [
+61
View File
@@ -0,0 +1,61 @@
# Website runtime source attribution
REA integrates native Chrome DevTools Protocol inspection and precise V8 coverage through two composable tools. Supply an existing loopback CDP endpoint and a page target from `list_browser_targets`. With omitted origins, REA selects the page's current HTTP(S) origin. The browser and page remain externally owned.
## Observe execution
```bash
rea observe-web-execution http://127.0.0.1:9222 TARGET_ID \
--observation-ms 10000 --json
```
MCP: `observe_web_execution` with `cdp_endpoint`, `target_id`, optional `allowed_origins` and optional `observation_ms` (default 10000). The operation itself selects instrumentation; no extra approval flag is needed.
Starting precise coverage resets counters and prevents optimized execution until REA stops it. This changes timing/JIT behavior. REA requests accurate call counts and detailed ranges, takes one resetting sample, stops coverage, disables enabled domains and detaches its own connection. It does not navigate, click, evaluate selected application code or close the page.
Wait for the actual armed progress event before performing an action. MCP emits negotiated progress notifications; CLI emits `rea_progress` JSON on stderr while the Evidence result stays on stdout. Use ordinary commands, scripts or user actions to trigger behavior. CLI SIGINT/SIGTERM and MCP cancellation await instrumentation cleanup; unconfirmed cleanup is an error with the target, failed commands and previous failure.
Results retain:
- The selected target, frame and document loader identity; local armed/end timestamps and separate backend monotonic coverage timestamps.
- Every admitted producer function/block range, exclusive end offset and execution count in UTF-16 code units, with verified bounds when exact text is available. Nested/overlapping counts are preserved and must not be summed.
- Main-document script inventory, exact session script IDs, resource start coordinates, sourceURL/source-map declarations, producer hash and independently computed UTF-8 text digest.
- Armed-window requests from the selected main frame and selected origins, complete reported initiator objects (including stack hierarchy/descriptions), derived callsites, exact script-ID associations and unresolved async parent IDs. Source association does not establish UI causality.
The coverage counter interval starts at backend acceptance before the armed receipt and ends at the resetting sample after the local timer. Counts may include execution during these command intervals; request events are restricted to the locally armed window. The result retains both backend timestamps and the distinct local clock.
Source metadata remains collected through receipt of the resetting sample, including scripts parsed during the final command. REA then freezes this inventory for source joins. Fatal protocol parsing failures terminate the observer even when no command is outstanding; listener inspection checks producer failures after its final document assertion.
A document replacement ends the window; final coverage then remains unavailable. Missing coverage is unknown: an already loaded script may be absent from a sample. A detailed request also does not guarantee block granularity for already compiled functions. If `is_block_coverage` is false, the function-only ranges are observed and branch execution remains unknown. Native Chrome verification reproduced block coverage in the first window and function-only coverage in a subsequent window, including without REA.
Same-document navigation such as `history.pushState()` remains valid when the frame and known loader identity stay stable and the current origin is allowed. Unknown loader identity retains a conservative URL check; newly reported or replaced loaders are rejected. Malformed producer events end an armed window immediately and trigger cleanup, independently of the selected duration.
Target detachment ends the window even when the browser's root connection stays open. When the closed session cannot acknowledge cleanup, the operation returns a failure with the selected target, failed commands and original producer reason; it does not claim successful cleanup.
Child frames, workers, service workers, WebAssembly, chronological execution order and framework-specific causal interpretation are outside this operation. Script ownership comes from frame/context metadata, never URL equality. Repeated URLs and synthetic names therefore retain separate identities.
Observed script metadata remains available when ownership is unknown; source text and source association remain excluded or unknown. Request/initiator resource URLs omit transport userinfo. Stack and script URLs can also be inert `sourceURL` names: REA preserves declared names and unknown representations, and only removes script URL userinfo when stable producer metadata explicitly reports `hasSourceURL: false`. This policy also applies when metadata arrives after a request. Unknown producer extension fields and source-map declarations are preserved.
## Inspect a node's listeners
```bash
rea inspect-web-event-listeners http://127.0.0.1:9222 TARGET_ID '#submit' --json
```
MCP: `inspect_web_event_listeners` with the same scope and an explicit CSS `selector`. Native DOM selection inspects the first matching node in the main document. A missing match is an explicit empty result.
The result contains directly registered listener types/flags, callback coordinates, source identity/text/digest and execution unknowns. REA uses native DOM/DOMDebugger operations and releases its own remote object group. It never invokes a listener or dispatches an event. Delegated ancestor listeners, descendants, closed shadow roots and framework registries are not enumerated.
## Bounds and verification
Each protocol message has a 64 MiB budget; script/context metadata and request metadata each have an 8 MiB retained budget. Complete retained source text has a 32 MiB budget. Oversized or malformed evidence returns no partial successful result. Each command and the complete inspection/source-join phase have a 20-second deadline; cleanup has a separate 5-second command deadline plus transport closure. The selected observation duration must fit the host timer's positive 32-bit millisecond range. REA does not control the externally owned browser's CPU or memory.
`npm run verify:browser:runtime` uses caller-supplied `REA_BROWSER_EXECUTABLE` and the native `ps` command on Linux/macOS against an owned synthetic fixture, through real public CLI and stdio MCP. It checks exact Unicode/CRLF text/digests, same-URL identities, same-document navigation, inert declared names, executed and zero-count branches, function-only unknowns on repeated observation, listener selection, initiator association, actual arming and preservation of the external page. A separate fixture page closes during an armed MCP window to verify prompt termination and inline cleanup diagnostics. The optional positional entrypoint to `scripts/verify-browser-runtime.mjs` permits the same checks against an isolated installed tarball.
Real workflow coverage is Linux x64 / Chrome 153 / Node 24. Other browser engines and host workflows remain unverified. The conditional `real-web-runtime` CI lane acquires Chrome only for relevant changes; ordinary unit/static checks need no browser.
Primary protocol references: [Profiler and Debugger](https://raw.githubusercontent.com/ChromeDevTools/devtools-protocol/master/pdl/js_protocol.pdl), [DOMDebugger](https://raw.githubusercontent.com/ChromeDevTools/devtools-protocol/master/pdl/domains/DOMDebugger.pdl).
Source ownership requires both the selected main frame and producer evidence for its default execution world. Extension/automation isolated worlds, worker worlds and unknown worlds cannot establish website source joins. Contradictory world metadata stays unowned. Referenced source records retain the reported execution context and script auxiliary context metadata; source text is read only for proven main-document default worlds.
Known script/context identities are monitored through asynchronous source completion after the sample cutoff. A changed identity excludes its retained text, frame attribution, coverage and request/source joins. Chromium's specific native CSS syntax rejection returns an input error for `selector`, including the selected target; unrelated node, command and transport failures retain their original category.
+12 -2
View File
@@ -3,6 +3,10 @@ set -euo pipefail
PACKAGE="rea-agents"
REPOSITORY="morluto/rea"
numeric_identifier='(0|[1-9][0-9]*)'
prerelease_identifier="(${numeric_identifier}|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)"
build_identifier='[0-9A-Za-z-]+'
version_pattern="${numeric_identifier}\\.${numeric_identifier}\\.${numeric_identifier}(-${prerelease_identifier}(\\.${prerelease_identifier})*)?(\\+${build_identifier}(\\.${build_identifier})*)?"
version="${REA_VERSION:-}"
dry_run=false
start_setup=true
@@ -67,14 +71,20 @@ if ! [[ "$_node_patch" =~ ^[0-9]+(\+[0-9A-Za-z.-]+)?$ ]] ||
fi
if [[ -n "$version" ]]; then
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] || fail "version must be an exact semantic version."
[[ "$version" =~ ^${version_pattern}$ ]] || fail "version must be an exact semantic version."
else
release_json="$(curl -fsSL --proto '=https' --tlsv1.2 --retry 3 -H 'Accept: application/vnd.github+json' "https://api.github.com/repos/$REPOSITORY/releases/latest")" || fail "the latest REA release could not be resolved. Check network access or pass --version VERSION, then retry."
tag="$(printf '%s' "$release_json" | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{const v=JSON.parse(s).tag_name;if(typeof v!=="string")process.exit(1);process.stdout.write(v)})' 2>/dev/null)" || fail "the release response was invalid. Retry later or pass --version VERSION."
[[ "$tag" =~ ^rea-agents-([0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?)$ ]] || fail "the latest release tag was invalid. Retry later or pass --version VERSION."
[[ "$tag" =~ ^rea-agents-(${version_pattern})$ ]] || fail "the latest release tag was invalid. Retry later or pass --version VERSION."
version="${BASH_REMATCH[1]}"
fi
# npm's exact-version resolver cleans build metadata from the requested version,
# so it may install the base release and make the post-install identity check fail.
if [[ "$version" =~ \+ ]]; then
fail "npm cannot install an exact version with build metadata. Pass a version without build metadata, then retry."
fi
prefix_args=()
if [[ "$platform" == "Linux" ]]; then
prefix_args=(--prefix "$HOME/.local")
+22 -5
View File
@@ -284,7 +284,7 @@ static Handle createPrivateFile(Runtime& root, const std::wstring& relative) {
verifyPrivate(parent->get(), parentPath, parentPath == root.path);
Security security;
auto file = createRelative(parent->get(), path.substr(path.find_last_of(L'\\') + 1),
GENERIC_WRITE | GENERIC_READ | READ_CONTROL, false, security, path);
DELETE | GENERIC_WRITE | GENERIC_READ | READ_CONTROL, false, security, path);
verifyPrivate(file.get(), path, false);
return file;
}
@@ -412,6 +412,18 @@ static void copySnapshot(napi_env, void* pointer) {
static void completeSnapshot(napi_env env, napi_status status, void* pointer) {
std::unique_ptr<SnapshotWork> work(static_cast<SnapshotWork*>(pointer));
work->root->snapshotPending = false;
const auto reject = [&](const Failure& failure) {
try {
dispose(work->destination.get(), work->path);
work->destination.reset();
napi_reject_deferred(env, work->deferred, failureValue(env, failure));
} catch (const Failure& cleanupFailure) {
const Failure combined(
failure.constraint + "; incomplete snapshot cleanup failed: " + cleanupFailure.constraint,
work->path, cleanupFailure.win32);
napi_reject_deferred(env, work->deferred, failureValue(env, combined));
}
};
try {
if (work->failure) throw *work->failure;
require(status == napi_ok && !work->root->snapshotCancelled.load(), "Native snapshot worker was cancelled", work->path, ERROR_OPERATION_ABORTED);
@@ -422,10 +434,9 @@ static void completeSnapshot(napi_env env, napi_status status, void* pointer) {
work->root->immutableFiles.push_back(std::move(work->destination));
check(napi_resolve_deferred(env, work->deferred, result));
} catch (const Failure& failure) {
napi_reject_deferred(env, work->deferred, failureValue(env, failure));
reject(failure);
} catch (const std::exception& failure) {
napi_reject_deferred(env, work->deferred,
failureValue(env, Failure(failure.what(), work->path, ERROR_GEN_FAILURE)));
reject(Failure(failure.what(), work->path, ERROR_GEN_FAILURE));
}
napi_delete_reference(env, work->rootReference);
napi_delete_async_work(env, work->work);
@@ -506,7 +517,13 @@ napi_value filesystemCall(napi_env env, const std::wstring& operation, const std
auto file = runtimeDirectory(root, path.substr(0, path.find_last_of(L'\\')));
file->path = path;
file->requestedPath = path;
file->handles.push_back(openComponent(path, GENERIC_READ, false));
// Retained output handles have write and DELETE access, so this read open
// must share both to pass Windows' symmetric share check. Those retained
// handles still omit FILE_SHARE_WRITE and FILE_SHARE_DELETE, blocking
// competing writes and delete opens.
file->handles.push_back(openComponent(path, GENERIC_READ, false,
FILE_SHARE_READ | FILE_SHARE_WRITE |
FILE_SHARE_DELETE));
verifyPrivate(file->get(), path, false);
auto result = identity(env, file->get(), path);
set(env, result, "handle", wrap(env, std::move(file)));
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "rea-agents",
"version": "4.1.0",
"version": "5.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rea-agents",
"version": "4.1.0",
"version": "5.0.0",
"license": "MIT",
"dependencies": {
"@babel/parser": "8.0.4",
+9 -8
View File
@@ -1,7 +1,7 @@
{
"name": "rea-agents",
"mcpName": "io.github.morluto/rea",
"version": "4.1.0",
"version": "5.0.0",
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
"license": "MIT",
"repository": {
@@ -83,7 +83,7 @@
"check:pr": "turbo run typecheck lint format:check knip metadata:check test:run && npm run docs:check && npm run jscpd && npm run scan:todos",
"precheck:ci": "npm run deps:check",
"check:ci": "turbo run typecheck lint format:check knip metadata:check jscpd scan:todos",
"build:cached": "turbo run build",
"build:cached": "node scripts/check-dependency-install.mjs && turbo run build",
"format": "oxfmt .",
"format:check": "oxfmt --check .",
"lint": "oxlint && npm run verify:module-boundaries",
@@ -121,17 +121,17 @@
"typecheck": "tsc --noEmit",
"verify:test-temp-hygiene": "npm run build:cached && node scripts/verify-test-temp-hygiene.mjs",
"verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs",
"verify:apple-dispatch": "npm run build:cached && node scripts/verify-apple-dispatch.mjs",
"verify:apple-dispatch": "npm run build:cached && node scripts/verify/apple/dispatch.mjs",
"verify:native-ui": "npm run build:cached && node scripts/verify-native-ui.mjs",
"verify:keyed-archive": "npm run build:cached && node scripts/verify-keyed-archive.mjs",
"verify:macos-bundle": "npm run build:cached && node scripts/verify-macos-bundle.mjs",
"verify:keyed-archive": "npm run build:cached && node scripts/verify/apple/keyed-archive.mjs",
"verify:macos-bundle": "npm run build:cached && node scripts/verify/apple/macos-bundle.mjs",
"verify:ghidra": "npm run build:cached && node scripts/verify-real-ghidra.mjs",
"verify:ida": "npm run build:cached && node scripts/verify-real-ida.mjs",
"verify:ghidra:switch": "npm run build:cached && node scripts/verify-real-ghidra-switch.mjs",
"verify:ghidra:dos": "npm run build:cached && node scripts/verify-real-ghidra-dos.mjs",
"verify:ghidra:com": "npm run build:cached && node scripts/verify-real-ghidra-com.mjs",
"verify:ghidra:aarch64-jump-table": "npm run build:cached && node scripts/verify-real-ghidra.mjs --aarch64-jump-table",
"verify:interface-builder": "npm run build:cached && node scripts/verify-interface-builder.mjs",
"verify:interface-builder": "npm run build:cached && node scripts/verify/apple/interface-builder.mjs",
"verify:ghidra:cross-format": "npm run build:cached && node scripts/verify-real-ghidra.mjs --cross-format",
"verify:ghidra:windows": "npm run build:cached && npm run build:ghidra-windows-fixture && node scripts/verify-real-ghidra-windows.mjs",
"verify:browser": "npm run build:cached && node scripts/verify-real-browser.mjs",
@@ -171,7 +171,7 @@
"docs:error-schema": "npm run build:cached && node scripts/generate-error-schema.mjs",
"prepack": "node scripts/prepack.mjs",
"prepare": "node scripts/prepare.mjs",
"verify:asset-catalog": "npm run build:cached && node scripts/verify-asset-catalog.mjs",
"verify:asset-catalog": "npm run build:cached && node scripts/verify/apple/asset-catalog.mjs",
"verify:native-ui:permissions": "npm run build:cached && node scripts/verify-native-ui.mjs --allow-permission-denial",
"test:focused": "node scripts/run-development-tests.mjs focused",
"build:windows-native": "node scripts/build-windows-native.mjs",
@@ -187,7 +187,8 @@
"verify:test-discovery": "node scripts/verify-test-discovery.mjs",
"verify:javascript:recovery": "node scripts/verify/javascript/recovery.mjs",
"verify:module-boundaries": "node scripts/verify-module-boundaries.mjs",
"verify:browser:source-maps": "npm run build:cached && node scripts/verify-browser-source-maps.mjs"
"verify:browser:source-maps": "npm run build:cached && node scripts/verify-browser-source-maps.mjs",
"verify:browser:runtime": "npm run build:cached && node scripts/verify-browser-runtime.mjs"
},
"lint-staged": {
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
+51
View File
@@ -0,0 +1,51 @@
import { createServer } from "node:http";
/** Owned synthetic site keeps source bytes, repeated URLs and expected execution independent of REA. */
export async function startBrowserRuntimeSite() {
let origin;
let evidenceRequests = 0;
const sources = () => ({
selected: `// 🔥 UTF-16 prefix\r\nfunction chosen() {\r\n history.pushState({}, '', '/same-document?caller=value#fragment');\r\n fetch('/evidence?marker=chosen');\r\n if (document.body.dataset.branch === 'unused') { fetch('/never'); }\r\n}\r\ndocument.querySelector('#run').addEventListener('click', chosen, { passive: true });\r\n//# sourceURL=${origin.replace("http://", "http://declared:label@")}/same.js\r\n`,
other: `function untouched() { return 'other source'; }\r\ndocument.querySelector('#child').addEventListener('click', untouched);\r\n//# sourceURL=${origin.replace("http://", "http://declared:label@")}/same.js\r\n`,
});
const server = createServer((request, response) => {
response.setHeader("content-type", "text/html; charset=utf-8");
if (request.url?.startsWith("/evidence")) {
evidenceRequests += 1;
response.end("ok");
return;
}
if (request.url === "/a.js" || request.url === "/b.js") {
response.setHeader(
"content-type",
"application/javascript; charset=utf-8",
);
response.end(
request.url === "/a.js" ? sources().selected : sources().other,
);
return;
}
response.end(
'<!doctype html><meta charset="utf-8"><button id="run"><span id="child">Run</span></button><script src="/a.js"></script><script src="/b.js"></script>',
);
});
await new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", resolve);
});
const address = server.address();
if (address === null || typeof address === "string")
throw new Error("Runtime fixture did not bind TCP");
origin = `http://127.0.0.1:${address.port}`;
return {
origin,
sources: sources(),
evidenceRequests: () => evidenceRequests,
close: () =>
new Promise((resolve, reject) =>
server.close((error) =>
error === undefined ? resolve() : reject(error),
),
),
};
}
+65 -7
View File
@@ -24,8 +24,10 @@ SUPPORTED_HOPPER_SHA256 = {
"1339f9e58377442b0c6fcb0dfc3cec20d593cc557408521fad9a00dbc6b8da13",
}
EXPECTED_SCREEN = (1280, 1024)
EXPECTED_DIALOG = (189, 370, 901, 284)
DEMO_CLICK = (305, 632)
DIALOG_TITLES = {"Registration", "Registration — Hopper Disassembler"}
HOPPER_WINDOW_TITLE = "Hopper Disassembler"
DEMO_CLICK_LEFT = 116
DEMO_CLICK_BOTTOM = 22
PRIVATE_DISPLAY_UNAVAILABLE = 70
X11_AUTHORIZATION_FAILED = 71
UNSUPPORTED_HOPPER_BUILD = 72
@@ -197,6 +199,47 @@ def windows(x11: ctypes.CDLL, display: int, root: int) -> list[tuple[int, int, i
return found
def window_title(x11: ctypes.CDLL, display: int, window: int) -> str | None:
title = ctypes.c_char_p()
if not x11.XFetchName(display, window, ctypes.byref(title)) or not title.value:
return None
try:
return title.value.decode("utf-8", "replace")
finally:
x11.XFree(ctypes.cast(title, ctypes.c_void_p))
def window_transient_for(
x11: ctypes.CDLL, display: int, window: int
) -> int | None:
parent = ctypes.c_ulong()
if not x11.XGetTransientForHint(display, window, ctypes.byref(parent)):
return None
return int(parent.value)
def find_demo_dialog(
x11: ctypes.CDLL,
display: int,
observed: list[tuple[int, int, int, int, int]],
) -> tuple[int, int, int, int, int] | None:
for candidate in observed:
window = candidate[0]
if window_title(x11, display, window) not in DIALOG_TITLES:
continue
parent = window_transient_for(x11, display, window)
if parent is None:
continue
if window_title(x11, display, parent) == HOPPER_WINDOW_TITLE:
return candidate
return None
def demo_click(dialog: tuple[int, int, int, int, int]) -> tuple[int, int]:
_window, x, y, _width, height = dialog
return x + DEMO_CLICK_LEFT, y + height - DEMO_CLICK_BOTTOM
def configure_x11(x11: ctypes.CDLL, xtst: ctypes.CDLL) -> None:
window_pointer = ctypes.POINTER(ctypes.c_ulong)
x11.XOpenDisplay.argtypes = [ctypes.c_char_p]
@@ -213,6 +256,18 @@ def configure_x11(x11: ctypes.CDLL, xtst: ctypes.CDLL) -> None:
ctypes.POINTER(XWindowAttributes),
]
x11.XGetWindowAttributes.restype = ctypes.c_int
x11.XFetchName.argtypes = [
ctypes.c_void_p,
ctypes.c_ulong,
ctypes.POINTER(ctypes.c_char_p),
]
x11.XFetchName.restype = ctypes.c_int
x11.XGetTransientForHint.argtypes = [
ctypes.c_void_p,
ctypes.c_ulong,
ctypes.POINTER(ctypes.c_ulong),
]
x11.XGetTransientForHint.restype = ctypes.c_int
x11.XQueryTree.argtypes = [
ctypes.c_void_p,
ctypes.c_ulong,
@@ -645,6 +700,7 @@ def run_launch(args: argparse.Namespace) -> int:
deadline = time.monotonic() + 10
observed: list[tuple[int, int, int, int, int]] = []
dialog: tuple[int, int, int, int, int] | None = None
while time.monotonic() < deadline:
if socket.exists():
x11.XCloseDisplay(display)
@@ -660,7 +716,8 @@ def run_launch(args: argparse.Namespace) -> int:
"owned Hopper process exited",
)
observed = windows(x11, display, root)
if any(window[1:] == EXPECTED_DIALOG for window in observed):
dialog = find_demo_dialog(x11, display, observed)
if dialog is not None:
break
time.sleep(0.1)
else:
@@ -676,8 +733,10 @@ def run_launch(args: argparse.Namespace) -> int:
"expected Hopper demo dialog not found",
)
assert dialog is not None
click_x, click_y = demo_click(dialog)
input_results = (
xtst.XTestFakeMotionEvent(display, -1, DEMO_CLICK[0], DEMO_CLICK[1], 0),
xtst.XTestFakeMotionEvent(display, -1, click_x, click_y, 0),
xtst.XTestFakeButtonEvent(display, 1, True, 0),
xtst.XTestFakeButtonEvent(display, 1, False, 0),
)
@@ -696,9 +755,8 @@ def run_launch(args: argparse.Namespace) -> int:
finally:
if display is not None:
x11.XCloseDisplay(display)
if child is not None and child.poll() is None:
child.terminate()
child.wait()
if child is not None:
stop_process(child)
stop_process(xvfb)
capture.close()
+445
View File
@@ -0,0 +1,445 @@
import assert from "node:assert/strict";
import { spawn, execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { once } from "node:events";
import { access } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { CdpBrowserProvider } from "../../dist/browser/CdpBrowserProvider.js";
import { CdpConnection } from "../../dist/browser/CdpConnection.js";
import { waitForBrowserDevtoolsPort } from "../../dist/browser/BrowserProcessStartup.js";
import { parseEvidence } from "../../dist/domain/evidence.js";
import { webExecutionSchema } from "../../dist/domain/webExecution.js";
import { webEventListenersSchema } from "../../dist/domain/webEventListeners.js";
import { startBrowserRuntimeSite } from "../fixtures/browser-runtime-site.mjs";
import { mcpTextValue } from "./mcp-verifier-results.mjs";
import {
startRuntimeFixtureBrowser,
closeRuntimeFixtureResources,
} from "./browser-runtime-fixture-lifecycle.mjs";
import { verifyRuntimeTargetTermination } from "./browser-runtime-termination-e2e.mjs";
import { verifyRuntimeInvalidSelectors } from "./browser-runtime-selector-e2e.mjs";
import {
createRuntimeIsolatedWorld,
triggerRuntimeFixtureAction,
isolatedFixtureUrl,
} from "./browser-runtime-world-e2e.mjs";
/** Exercise native browser runtime attribution through public CLI and stdio MCP, including arming. */
export async function verifyBrowserRuntime(
executable,
entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url)),
) {
await access(executable);
const site = await startBrowserRuntimeSite();
let fixture;
let primaryError;
let stderr = "";
let action;
let client;
try {
fixture = await startRuntimeFixtureBrowser(executable, site.origin);
const { browser, profile } = fixture;
browser.stderr.on("data", (chunk) => {
if (stderr.length < 65536) stderr += chunk;
});
const port = await waitForBrowserDevtoolsPort({
child: browser,
executable,
activePortPath: join(profile, "DevToolsActivePort"),
stderr: () => stderr,
timeoutMs: 20_000,
});
const endpoint = `http://127.0.0.1:${port}`;
const targets = await new CdpBrowserProvider().listTargets({
cdp_endpoint: endpoint,
allowed_origins: [site.origin],
});
if (!targets.ok) throw targets.error;
const target = targets.value.targets[0];
assert.ok(target, "owned fixture page target missing");
const pages = await (await fetch(`${endpoint}/json/list`)).json();
const page = pages.find((candidate) => candidate.id === target.target_id);
action = await CdpConnection.connect(
page.webSocketDebuggerUrl,
"inspect_web_page",
);
await ready(action);
let isolatedContext = await createRuntimeIsolatedWorld(action);
const env = {
...process.env,
REA_LOG_LEVEL: "silent",
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
};
const input = { cdp_endpoint: endpoint, target_id: target.target_id };
const cliListener = await promisify(execFile)(
process.execPath,
[
entrypoint,
"inspect-web-event-listeners",
endpoint,
target.target_id,
"#run",
"--json",
],
{ env, timeout: 40_000, maxBuffer: 64 * 1024 * 1024 },
);
const cliListenerProof = assertListeners(
parseEvidence(JSON.parse(cliListener.stdout)),
site,
);
const cliExecution = await executionCli(
entrypoint,
input,
env,
action,
isolatedContext,
);
const cliProof = assertExecution(
parseEvidence(JSON.parse(cliExecution)),
site,
);
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
client = new Client({ name: "web-runtime-real-e2e", version: "1" });
await client.connect(transport);
const invalidSelectors = await verifyRuntimeInvalidSelectors(
client,
entrypoint,
input,
env,
);
const listenerResponse = await client.callTool({
name: "inspect_web_event_listeners",
arguments: { ...input, selector: "#run" },
});
assert.notEqual(
listenerResponse.isError,
true,
mcpTextValue(listenerResponse),
);
const mcpListenerProof = assertListeners(
parseEvidence(JSON.parse(mcpTextValue(listenerResponse)).evidence),
site,
);
const mcpProof = await executionMcp(
client,
input,
action,
site,
isolatedContext,
false,
);
assert.equal(cliProof.selected_sha256, mcpProof.selected_sha256);
assert.equal(cliListenerProof.sha256, mcpListenerProof.sha256);
await reloadFixture(action);
isolatedContext = await createRuntimeIsolatedWorld(action);
const freshMcpProof = await executionMcp(
client,
input,
action,
site,
isolatedContext,
);
assert.equal(site.evidenceRequests(), 3);
const termination = await verifyRuntimeTargetTermination(
client,
input,
site.origin,
);
const stillOpen = await action.send("Runtime.evaluate", {
expression: "Boolean(document.querySelector('#run'))",
returnByValue: true,
});
assert.equal(
stillOpen.result.value,
true,
"REA must preserve the externally owned page",
);
const location = await action.send("Runtime.evaluate", {
expression: "location.href",
returnByValue: true,
});
assert.equal(
location.result.value,
`${site.origin}/same-document?caller=value#fragment`,
);
return {
product: target,
cli: cliProof,
mcp: mcpProof,
listener: cliListenerProof,
public_cases: 8,
invalid_selectors: invalidSelectors,
termination,
fresh_mcp: freshMcpProof,
page_remained_open: true,
};
} catch (cause) {
primaryError = cause;
throw cause;
} finally {
await closeRuntimeFixtureResources(
[
() => client?.close(),
() => action?.close(),
() => fixture?.close(),
() => site.close(),
],
primaryError,
);
}
}
async function executionMcp(
client,
input,
action,
site,
isolatedContext,
requireBlock = true,
) {
let actionPromise;
const response = await client.callTool(
{
name: "observe_web_execution",
arguments: { ...input, observation_ms: 300 },
},
{
timeout: 40_000,
onprogress: (notification) => {
if (
actionPromise === undefined &&
notification.message?.includes(
"browser_execution: Browser execution observation armed",
)
) {
actionPromise = triggerRuntimeFixtureAction(action, isolatedContext);
void actionPromise.catch(() => undefined);
}
},
},
);
assert.ok(actionPromise, "MCP did not emit an actual armed notification");
await actionPromise;
assert.notEqual(response.isError, true, mcpTextValue(response));
return assertExecution(
parseEvidence(JSON.parse(mcpTextValue(response)).evidence),
site,
requireBlock,
);
}
async function ready(connection) {
for (let attempt = 0; attempt < 100; attempt += 1) {
const result = await connection.send("Runtime.evaluate", {
expression:
"typeof chosen === 'function' && typeof untouched === 'function'",
returnByValue: true,
});
if (result.result.value === true) return;
await new Promise((resolve) => setTimeout(resolve, 20));
}
throw new Error("Runtime fixture sources did not load");
}
async function executionCli(entrypoint, input, env, action, isolatedContext) {
const child = spawn(
process.execPath,
[
entrypoint,
"observe-web-execution",
input.cdp_endpoint,
input.target_id,
"--observation-ms",
"300",
"--json",
],
{ env, stdio: ["ignore", "pipe", "pipe"] },
);
let stdout = "";
let stderr = "";
let actionPromise;
const timeout = setTimeout(() => child.kill("SIGTERM"), 40_000);
child.stdout.on("data", (chunk) => {
stdout += chunk;
});
child.stderr.on("data", (chunk) => {
stderr += chunk;
if (actionPromise === undefined && stderr.includes('"completed":1')) {
actionPromise = triggerRuntimeFixtureAction(action, isolatedContext);
void actionPromise.catch(() => undefined);
}
});
try {
const [code] = await once(child, "close");
assert.equal(code, 0, stderr);
assert.ok(actionPromise, "CLI did not report an actual armed point");
await actionPromise;
return stdout;
} finally {
clearTimeout(timeout);
if (child.exitCode === null && child.signalCode === null)
child.kill("SIGTERM");
}
}
function sourceByText(result, text) {
const source = result.sources.find(
(item) => item.source.state === "captured" && item.source.text === text,
);
assert.ok(source, "Exact independently known source text must be retained");
assert.equal(
source.source.sha256,
createHash("sha256").update(text).digest("hex"),
);
assert.equal(source.source.utf16_units, text.length);
assert.equal(source.source.utf8_bytes, Buffer.byteLength(text));
return source;
}
function assertListeners(evidence, site) {
const result = webEventListenersSchema.parse(evidence.normalized_result);
assert.equal(
result.listeners.length,
1,
"The selected node's child listener must not be attributed to the selected node",
);
const listener = result.listeners.find((item) => item.type === "click");
assert.ok(listener);
assert.equal(listener.passive, true);
const source = sourceByText(result, site.sources.selected);
assert.equal(listener.location.script_id, source.script_id);
assert.equal(listener.location.source_association, "script_id");
assert.equal(listener.execution, "unknown");
return {
sha256: source.source.sha256,
line: listener.location.line_number,
column: listener.location.column_number,
};
}
function assertExecution(evidence, site, requireBlock = true) {
const result = webExecutionSchema.parse(evidence.normalized_result);
assert.ok(
result.coverage.scripts.every(
(item) => item.reported_url !== isolatedFixtureUrl,
),
);
assert.ok(
result.sources.every(
(item) =>
item.url !== isolatedFixtureUrl || item.source.state !== "captured",
),
);
const selected = sourceByText(result, site.sources.selected);
const other = sourceByText(result, site.sources.other);
assert.equal(selected.url, other.url);
assert.equal(selected.has_source_url, true);
assert.equal(
selected.url,
`${site.origin.replace("http://", "http://declared:label@")}/same.js`,
);
assert.notEqual(selected.script_id, other.script_id);
assert.ok(
selected.source.utf8_bytes > selected.source.utf16_units,
"Unicode oracle must distinguish UTF-8 bytes from UTF-16 units",
);
const coverage = result.coverage.scripts.find(
(script) => script.script_id === selected.script_id,
);
const fn = coverage?.functions.find((item) => item.name === "chosen");
assert.ok(
fn,
"Externally invoked callback must have precise function evidence",
);
assert.ok(fn.ranges.some((range) => range.count === 1));
if (requireBlock)
assert.equal(
fn.is_block_coverage,
true,
"Fresh fixture function must supply block granularity",
);
if (fn.is_block_coverage)
assert.ok(
fn.ranges.some((range) => range.count === 0),
`Unexecuted branch must remain zero within block coverage: ${JSON.stringify(fn)}`,
);
assert.equal(
fn.ranges[0].start_offset,
site.sources.selected.indexOf("function chosen"),
"Coverage offsets must index the retained UTF-16 source, including the astral prefix",
);
for (const range of fn.ranges) {
assert.equal(range.source_bounds, "verified");
assert.ok(range.end_offset <= selected.source.utf16_units);
}
const request = result.requests.find((item) =>
item.url.endsWith("/evidence?marker=chosen"),
);
assert.ok(request, "The selected request initiator must be retained");
assert.ok(
request.callsites.some(
(site) =>
site.script_id === selected.script_id &&
site.source_association === "script_id",
),
);
assert.equal(request.causal_attribution, "unknown");
assert.ok(
request.callsites.some(
(site) =>
site.script_id === selected.script_id && site.url === selected.url,
),
);
assert.equal(result.script_inventory.coverage_absence, "unknown");
assert.equal(result.instrumentation.cleanup, "confirmed");
return {
selected_sha256: selected.source.sha256,
other_sha256: other.source.sha256,
zero_branch: fn.is_block_coverage
? "observed-zero"
: "unknown-function-only",
granularity: fn.is_block_coverage ? "block" : "function",
repeated_url_identity: true,
same_document_navigation: true,
declared_source_url_preserved: true,
request_script_id: selected.script_id,
isolated_execution_proved_and_excluded: true,
};
}
async function reloadFixture(connection) {
await connection.send("Page.enable");
let dispose;
const committed = new Promise((resolve, reject) => {
const timer = setTimeout(
() => reject(new Error("Fixture reload did not commit")),
5000,
);
const remove = connection.onEvent((event) => {
if (
event.method === "Page.frameNavigated" &&
!event.params.frame?.parentId
)
resolve();
});
dispose = () => {
clearTimeout(timer);
remove();
};
});
void committed.catch(() => undefined);
try {
await connection.send("Page.reload", { ignoreCache: true });
await committed;
await ready(connection);
} finally {
dispose();
}
}
@@ -0,0 +1,153 @@
import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import { once } from "node:events";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawnOwnedProviderProcess } from "../../dist/process/ProviderProcess.js";
import { createSystemProcessOwnershipHost } from "../../dist/process/ProcessOwnershipObservation.js";
import { liveProcesses } from "../../dist/process/ProcessOwnershipProcessTree.js";
import { CdpConnection } from "../../dist/browser/CdpConnection.js";
/** Launch only the synthetic fixture browser in its own verified process group. */
export async function startRuntimeFixtureBrowser(executable, origin) {
const profile = await mkdtemp(join(tmpdir(), "rea-real-web-runtime-"));
let owned;
try {
owned = await spawnOwnedProviderProcess({
command: executable,
arguments: [
"--headless=new",
...(process.env.REA_BROWSER_NO_SANDBOX === "true"
? ["--no-sandbox"]
: []),
"--remote-debugging-address=127.0.0.1",
"--remote-debugging-port=0",
`--user-data-dir=${profile}`,
"--no-first-run",
"--no-default-browser-check",
"--disable-background-networking",
"--disable-component-update",
"--disable-dev-shm-usage",
"--disable-sync",
origin,
],
runId: randomUUID(),
// Installed Chrome launch scripts exec another binary; do not require an argv prefix.
expectedCommand: null,
});
owned.process.stdout?.resume();
return {
browser: owned.process,
profile,
close: async () => {
await stopOwnedBrowser(owned, profile);
await rm(profile, {
recursive: true,
force: true,
maxRetries: 5,
retryDelay: 100,
});
},
};
} catch (cause) {
if (owned !== undefined) await stopOwnedBrowser(owned, profile);
await rm(profile, {
recursive: true,
force: true,
maxRetries: 5,
retryDelay: 100,
});
throw cause;
}
}
async function stopOwnedBrowser(owned, profile) {
const child = owned.process;
const exited =
child.exitCode === null && child.signalCode === null
? once(child, "exit")
: Promise.resolve();
void exited.catch(() => undefined);
let connection;
let timer;
const escalation = setTimeout(() => {
if (child.exitCode === null && child.signalCode === null)
child.kill("SIGKILL");
}, 2_000);
try {
if (child.exitCode === null && child.signalCode === null) {
try {
const [port, browserPath] = (
await readFile(join(profile, "DevToolsActivePort"), "utf8")
)
.trim()
.split("\n");
assert.match(port, /^\d+$/);
assert.match(browserPath, /^\/devtools\/browser\/[^\s]+$/);
const signal = AbortSignal.timeout(2_000);
connection = await CdpConnection.connect(
`ws://127.0.0.1:${port}${browserPath}`,
"inspect_web_page",
signal,
);
// This browser belongs to the verifier; public REA operations never close it.
await connection.send("Browser.close", {}, undefined, signal);
} catch {
if (child.exitCode === null && child.signalCode === null)
child.kill("SIGTERM");
}
}
if (owned.cleanup !== undefined) {
const result = await owned.cleanup();
assert.equal(result.cleaned, true, JSON.stringify(result));
}
await Promise.race([
exited,
new Promise((_, reject) => {
timer = setTimeout(
() =>
reject(
new Error("Owned fixture browser did not exit after shutdown"),
),
5_000,
);
}),
]);
} finally {
clearTimeout(escalation);
clearTimeout(timer);
await connection?.close();
}
if (process.platform === "win32") return;
const host = createSystemProcessOwnershipHost();
const deadline = Date.now() + 5_000;
while (true) {
const members = liveProcesses(await host.listProcesses()).filter(
(member) => member.processGroupId === owned.ownership.processGroupId,
);
if (members.length === 0) return;
if (Date.now() >= deadline)
throw new Error(
`Owned fixture group remains: ${JSON.stringify(members.map(({ pid }) => pid))}`,
);
await new Promise((resolve) => setTimeout(resolve, 25));
}
}
/** Attempt every owned cleanup and retain the original verification failure if cleanup also fails. */
export async function closeRuntimeFixtureResources(tasks, primaryError) {
const failures = [];
for (const task of tasks) {
try {
await task();
} catch (cause) {
failures.push(cause);
}
}
if (failures.length > 0)
throw new AggregateError(
primaryError === undefined ? failures : [primaryError, ...failures],
"Runtime fixture cleanup failed",
);
}
@@ -0,0 +1,49 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { mcpTextValue } from "./mcp-verifier-results.mjs";
/** Verify native malformed-selector classification through the public CLI and MCP. */
export async function verifyRuntimeInvalidSelectors(
client,
entrypoint,
input,
env,
) {
const cli = await new Promise((resolve) => {
execFile(
process.execPath,
[
entrypoint,
"inspect-web-event-listeners",
input.cdp_endpoint,
input.target_id,
"[",
"--json",
],
{ env, timeout: 40_000, maxBuffer: 64 * 1024 * 1024 },
(error, stdout, stderr) => resolve({ error, stdout, stderr }),
);
});
assert.ok(cli.error, "Invalid native CSS must fail in the CLI");
assert.equal(typeof cli.error.code, "number", cli.stderr);
assertInputFailure(JSON.parse(cli.stdout), input.target_id);
const response = await client.callTool({
name: "inspect_web_event_listeners",
arguments: { ...input, selector: "[" },
});
assert.equal(response.isError, true, mcpTextValue(response));
assertInputFailure(JSON.parse(mcpTextValue(response)).error, input.target_id);
return { cli: "invalid_input", mcp: "invalid_input", selector: "[" };
}
function assertInputFailure(failure, targetId) {
assert.equal(failure.code, "invalid_request");
assert.equal(failure.category, "invalid_input");
assert.equal(failure.details.operation, "inspect_web_event_listeners");
assert.ok(
failure.details.issues.some(
(issue) =>
issue.path[0] === "selector" && issue.message.includes(targetId),
),
);
}
@@ -0,0 +1,104 @@
import assert from "node:assert/strict";
import { CdpConnection } from "../../dist/browser/CdpConnection.js";
import { mcpTextValue } from "./mcp-verifier-results.mjs";
import { closeRuntimeFixtureResources } from "./browser-runtime-fixture-lifecycle.mjs";
/** Close a separately owned fixture page after actual MCP arming and inspect native cleanup diagnostics. */
export async function verifyRuntimeTargetTermination(client, input, origin) {
const version = await (
await fetch(`${input.cdp_endpoint}/json/version`)
).json();
const root = await CdpConnection.connect(
version.webSocketDebuggerUrl,
"observe_web_execution",
);
let targetId;
let closed = false;
let primaryError;
try {
({ targetId } = await root.send("Target.createTarget", { url: origin }));
assert.equal(typeof targetId, "string");
await waitForTarget(input.cdp_endpoint, targetId, origin);
let closePromise;
const response = await client.callTool(
{
name: "observe_web_execution",
arguments: {
...input,
target_id: targetId,
observation_ms: 2_147_483_647,
},
},
{
timeout: 20_000,
onprogress: (notification) => {
if (
closePromise === undefined &&
notification.message?.includes(
"browser_execution: Browser execution observation armed",
)
) {
closePromise = root
.send("Target.closeTarget", { targetId })
.then((result) => {
assert.equal(result.success, true);
closed = true;
});
void closePromise.catch(() => undefined);
}
},
},
);
assert.ok(
closePromise,
"The separate page must be closed after actual arming",
);
await closePromise;
assert.equal(
response.isError,
true,
"Detached instrumentation cannot claim confirmed cleanup",
);
const failure = mcpTextValue(response);
assert.match(failure, /cleanup.*confirmed/u);
assert.ok(
failure.includes(targetId),
"Failure must retain the selected target identity",
);
assert.match(failure, /Profiler\.stopPreciseCoverage/u);
return {
target_id: targetId,
page_closed_after_arming: true,
failure_inline: true,
cleanup: "unconfirmed_reported",
};
} catch (cause) {
primaryError = cause;
throw cause;
} finally {
await closeRuntimeFixtureResources(
[
() =>
targetId === undefined || closed
? undefined
: root.send("Target.closeTarget", { targetId }),
() => root.close(),
],
primaryError,
);
}
}
async function waitForTarget(endpoint, targetId, origin) {
for (let attempt = 0; attempt < 100; attempt += 1) {
const pages = await (await fetch(`${endpoint}/json/list`)).json();
if (
pages.some((page) => page.id === targetId && page.url.startsWith(origin))
)
return;
await new Promise((resolve) => setTimeout(resolve, 20));
}
throw new Error(
"Separate fixture target did not settle on its selected origin",
);
}
+34
View File
@@ -0,0 +1,34 @@
import assert from "node:assert/strict";
export const isolatedFixtureUrl = "rea-isolated-fixture.js";
/** Create an explicitly owned native isolated-world fixture outside REA's public operations. */
export async function createRuntimeIsolatedWorld(connection) {
const tree = await connection.send("Page.getFrameTree");
const world = await connection.send("Page.createIsolatedWorld", {
frameId: tree.frameTree.frame.id,
worldName: "rea-verifier-isolated",
});
const contextId = world.executionContextId;
assert.ok(Number.isInteger(contextId));
const initialized = await connection.send("Runtime.evaluate", {
contextId,
expression: `function reaIsolatedFixture() { return 'isolated-proof'; }
//# sourceURL=${isolatedFixtureUrl}`,
});
assert.equal(initialized.exceptionDetails, undefined);
return contextId;
}
/** Invoke a known isolated callback and the selected site's callback after actual REA arming. */
export async function triggerRuntimeFixtureAction(connection, contextId) {
const proof = await connection.send("Runtime.evaluate", {
contextId,
expression: "reaIsolatedFixture()",
returnByValue: true,
});
assert.equal(proof.result.value, "isolated-proof");
return connection.send("Runtime.evaluate", {
expression: "document.querySelector('#run').click()",
});
}
+5 -1
View File
@@ -46,6 +46,7 @@ export const toolFamilyCatalog = (sources) => {
surface: "browser-provider",
contracts: [
...sources.browserContracts.BROWSER_TOOL_CONTRACTS,
...sources.webRuntimeContracts.WEB_RUNTIME_TOOL_CONTRACTS,
...sources.browserScenarioContracts.BROWSER_SCENARIO_TOOL_CONTRACTS,
],
},
@@ -171,7 +172,10 @@ export const providerCatalog = (sources) => {
},
{
identity: sources.browserProvider.CDP_BROWSER_PROVIDER_IDENTITY,
contracts: sources.browserContracts.BROWSER_TOOL_CONTRACTS,
contracts: [
...sources.browserContracts.BROWSER_TOOL_CONTRACTS,
...sources.webRuntimeContracts.WEB_RUNTIME_TOOL_CONTRACTS,
],
},
{
identity:
+2 -1
View File
@@ -40,7 +40,7 @@ const SOURCE_PATHS = {
officialContracts: "dist/contracts/officialToolContracts.js",
enhancedContracts: "dist/contracts/enhancedToolContracts.js",
sessionContracts: "dist/contracts/sessionToolContracts.js",
nativeContracts: "dist/contracts/nativeToolContracts.js",
nativeContracts: "dist/contracts/native/nativeToolContracts.js",
artifactContracts: "dist/contracts/artifactToolContracts.js",
managedContracts: "dist/contracts/managed/managedToolContracts.js",
firmwareContracts: "dist/contracts/firmware/firmwareToolContracts.js",
@@ -50,6 +50,7 @@ const SOURCE_PATHS = {
managedWorkflowContracts:
"dist/contracts/managed/managedWorkflowToolContracts.js",
browserContracts: "dist/contracts/browserToolContracts.js",
webRuntimeContracts: "dist/contracts/webRuntimeToolContracts.js",
browserScenarioContracts: "dist/contracts/browserScenarioToolContracts.js",
electronContracts: "dist/contracts/javascript/electronToolContracts.js",
javascriptRuntimeObservationContracts:
+4 -1
View File
@@ -69,6 +69,7 @@ const failedBoundary = (file, target) => {
return "provider-construction";
if (
(file.startsWith("src/artifacts/javascript/") ||
file.startsWith("src/artifacts/apple/") ||
/^src\/artifacts\/ArtifactHash\.(?:js|ts)$/u.test(file)) &&
(["application", "composition", "server", "cli", "main"].includes(
targetLayer,
@@ -88,12 +89,14 @@ const failedBoundary = (file, target) => {
)
return "pure-layer";
}
if (file === "src/application/runtime.ts") return undefined;
if (layer === "application" && targetLayer === "composition")
return "application-composition";
if (
(layer === "application" || layer === "server") &&
(MIGRATED_PROVIDER_ROOTS.has(targetLayer) ||
/^src\/(?:artifacts\/ArtifactProvider|dotnet\/ManagedStaticProvider|native\/NativeMacOSProvider)\.[^/]+$/u.test(
target,
) ||
/^src\/browser\/[^/]*Provider\.[^/]+$/u.test(target))
)
return "provider-construction";
+1 -1
View File
@@ -7,7 +7,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseEvidence } from "../../dist/domain/evidence.js";
import { analysisProfileSchema } from "../../dist/domain/analysisProfile.js";
import { nativeValueTraceSchema } from "../../dist/domain/nativeValueTrace.js";
import { nativeValueTraceSchema } from "../../dist/domain/native/nativeValueTrace.js";
const defaultEntrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
const environment = () => ({
+40
View File
@@ -0,0 +1,40 @@
"""Package the public Notes example for the static website."""
from io import BytesIO
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
EXAMPLE_FILES = (
"package.json",
"main.js",
"preload.js",
"renderer.js",
"csv.js",
"index.html",
)
def prepare_notes_example(site):
"""Create a ZIP with public source only and reproducible metadata."""
source = site / "examples/notes-electron"
contents = BytesIO()
with ZipFile(contents, "w") as archive:
for name in EXAMPLE_FILES:
entry = ZipInfo(f"notes-example/{name}", (1980, 1, 1, 0, 0, 0))
entry.create_system = 3
entry.external_attr = 0o100644 << 16
archive.writestr(
entry,
(source / name).read_bytes(),
compress_type=ZIP_DEFLATED,
compresslevel=9,
)
destination = site / "examples/notes-example.zip"
destination.write_bytes(contents.getvalue())
print(f"Prepared examples/notes-example.zip ({len(EXAMPLE_FILES)} files).")
if __name__ == "__main__":
root = Path(__file__).resolve().parent.parent
prepare_notes_example(root / "website/public")
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env node
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { verifyBrowserRuntime } from "./lib/browser-runtime-e2e.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
const executable = process.env.REA_BROWSER_EXECUTABLE;
if (executable === undefined)
throw new Error(
"verify:browser:runtime requires caller-supplied REA_BROWSER_EXECUTABLE",
);
if (process.platform !== "win32") {
try {
await promisify(execFile)("ps", ["-p", String(process.pid), "-o", "pid="], {
timeout: 5_000,
});
} catch (cause) {
throw new Error(
"verify:browser:runtime requires the host ps command for owned fixture cleanup",
{ cause },
);
}
}
const run = createVerifierRun();
const result = await verifyBrowserRuntime(executable, process.argv[2]);
console.log(
JSON.stringify(
{ status: "passed", ...result, verifier: await completeVerifierRun(run) },
null,
2,
),
);
+33 -1
View File
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
import { observeNativeUi } from "../dist/native/NativeUiObservation.js";
import { NATIVE_UI_HELPER_MAX_BUFFER } from "../dist/native/NativeUiOutputBudget.js";
if (process.platform !== "darwin")
throw new Error(
@@ -30,7 +31,38 @@ try {
"-o",
childRetrievalTest,
]);
await promisify(execFile)(childRetrievalTest, []);
const childRetrieval = await promisify(execFile)(childRetrievalTest, []);
const [slashRichProducerJson, numericProducerJson] = childRetrieval.stdout
.trimEnd()
.split("\n");
if (
slashRichProducerJson === undefined ||
numericProducerJson === undefined
) {
throw new Error(
"Native UI serializer seam did not emit its producer bytes",
);
}
const slashRichConsumerJson = JSON.stringify(
JSON.parse(slashRichProducerJson),
);
if (slashRichProducerJson !== slashRichConsumerJson) {
throw new Error(
"Swift helper JSON bytes differ from the normalized JavaScript consumer representation",
);
}
const numericConsumerJson = JSON.stringify(JSON.parse(numericProducerJson));
if (
Buffer.byteLength(numericProducerJson) <=
Buffer.byteLength(numericConsumerJson)
) {
throw new Error(
"Foundation numeric fixture no longer exercises larger producer JSON bytes",
);
}
if (Buffer.byteLength(numericProducerJson) > NATIVE_UI_HELPER_MAX_BUFFER) {
throw new Error("Foundation numeric fixture exceeds the raw helper budget");
}
const contents = join(root, "Fixture.app", "Contents");
await mkdir(join(contents, "MacOS"), { recursive: true });
+2 -2
View File
@@ -17,8 +17,8 @@ import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseEvidence } from "../dist/domain/evidence.js";
import { nativeDataTypeSchema } from "../dist/domain/nativeDataType.js";
import { nativeLoadImageSchema } from "../dist/domain/nativeLoadImage.js";
import { nativeDataTypeSchema } from "../dist/domain/native/nativeDataType.js";
import { nativeLoadImageSchema } from "../dist/domain/native/nativeLoadImage.js";
const mode = process.argv[2] ?? "symbols";
if (
+26 -1
View File
@@ -6,6 +6,7 @@ import re
import sys
from urllib.parse import unquote, urlsplit
import xml.etree.ElementTree as ET
from zipfile import BadZipFile, ZipFile
class HtmlReferences(HTMLParser):
@@ -89,15 +90,39 @@ def check_publisher(root):
return []
def check_example_archive(site):
"""Verify the downloadable ZIP contains the current six source files."""
source = site / "examples/notes-electron"
expected = {
f"notes-example/{name}": source / name
for name in ("package.json", "main.js", "preload.js", "renderer.js", "csv.js", "index.html")
}
path = site / "examples/notes-example.zip"
if not path.is_file():
return ["Notes example ZIP is missing; run python3 scripts/prepare-website.py."]
try:
with ZipFile(path) as archive:
if sorted(archive.namelist()) != sorted(expected):
return ["Notes example ZIP must contain exactly the six files in notes-example/."]
return [
f"Notes example ZIP has stale source: {name}"
for name, original in expected.items()
if archive.read(name) != original.read_bytes()
]
except (BadZipFile, RuntimeError) as error:
return [f"Notes example ZIP cannot be read: {error}"]
def main():
"""Run the same checks locally, on website PRs and before publishing."""
root = Path(__file__).resolve().parent.parent
pages, references, errors = check_site(root / "website/public")
errors.extend(check_example_archive(root / "website/public"))
errors.extend(check_publisher(root))
if errors:
print("Website checks failed:\n" + "\n".join(errors), file=sys.stderr)
return 1
print(f"Verified {pages} HTML pages, {references} local references and SVG XML.")
print(f"Verified {pages} HTML pages, {references} local references, SVG XML and the example ZIP.")
print("website-pages.yml is the sole GitHub Pages publisher.")
return 0
+51
View File
@@ -263,6 +263,57 @@ try {
]);
native.call("runtime_snapshot_cancel", [runtime.handle]);
await assert.rejects(cancelled, /cancelled/u);
assert.equal(await exists(join(runtime.path, "cancelled.bin")), false);
const readbackRuntime = WindowsPrivateRuntime.create(
workspace.replaceAll("\\", "/"),
"readback-",
);
try {
readbackRuntime.writeFile("written.txt", "runtime-write-readback");
assert.equal(
readbackRuntime.readFile("written.txt"),
"runtime-write-readback",
);
const readbackSnapshot = await readbackRuntime.snapshot(
source,
"snapshot.bin",
);
assert.equal(
readbackSnapshot.sha256,
createHash("sha256")
.update(await readFile(source))
.digest("hex"),
);
assert.equal(
readbackRuntime.readFile("snapshot.bin"),
await readFile(source, "utf8"),
);
const writtenPath = join(readbackRuntime.observation.path, "written.txt");
await assert.rejects(writeFile(writtenPath, "replace"));
await assert.rejects(rename(writtenPath, `${writtenPath}.renamed`));
await assert.rejects(rm(writtenPath));
assert.equal(
readbackRuntime.readFile("written.txt"),
"runtime-write-readback",
);
const snapshotPath = join(
readbackRuntime.observation.path,
"snapshot.bin",
);
await assert.rejects(rename(snapshotPath, `${snapshotPath}.renamed`));
await assert.rejects(rm(snapshotPath));
assert.equal(
readbackRuntime.readFile("snapshot.bin"),
await readFile(source, "utf8"),
);
report.controls = {
...report.controls,
completedRuntimeFileReadback: true,
completedRuntimeFileMutationDenied: true,
};
} finally {
await readbackRuntime.close();
}
report.controls = {
...report.controls,
protectedDaclReadback: true,
@@ -10,7 +10,7 @@ import {
artifactCli,
artifactMcpResult,
withArtifactMcp,
} from "./lib/artifact-e2e.mjs";
} from "../../lib/artifact-e2e.mjs";
if (process.platform !== "darwin")
throw new Error(
@@ -30,7 +30,7 @@ try {
"clang",
fileURLToPath(
new URL(
"../tests/conformance/interface-builder/fixture.c",
"../../../tests/conformance/interface-builder/fixture.c",
import.meta.url,
),
),
@@ -44,7 +44,10 @@ try {
await exec("/usr/bin/xcrun", [
"actool",
fileURLToPath(
new URL("../tests/conformance/native/Assets.xcassets", import.meta.url),
new URL(
"../../../tests/conformance/native/Assets.xcassets",
import.meta.url,
),
),
"--compile",
resources,
@@ -4,8 +4,8 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
import { inspectAppleDispatchMetadata } from "../dist/native/AppleDispatchMetadata.js";
import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js";
import { inspectAppleDispatchMetadata } from "../../../dist/native/AppleDispatchMetadata.js";
if (process.platform !== "darwin")
throw new Error(
@@ -14,7 +14,7 @@ if (process.platform !== "darwin")
const exec = promisify(execFile);
const source = (name) =>
fileURLToPath(
new URL(`../tests/conformance/native/${name}`, import.meta.url),
new URL(`../../../tests/conformance/native/${name}`, import.meta.url),
);
/** Both link modes: legacy LC_DYLD_INFO binds and the default chained fixups. */
@@ -8,8 +8,8 @@ import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
import { ArtifactProvider } from "../dist/artifacts/ArtifactProvider.js";
import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js";
import { ArtifactProvider } from "../../../dist/artifacts/ArtifactProvider.js";
const exec = promisify(execFile);
const fixtureRoot = await mkdtemp(join(tmpdir(), "rea-interface-builder-"));
@@ -18,7 +18,7 @@ const resourcesPath = join(appPath, "Contents", "Resources");
const executablePath = join(appPath, "Contents", "MacOS", "Fixture");
const nibPath = join(resourcesPath, "Main.nib");
const sourceRoot = fileURLToPath(
new URL("../tests/conformance/interface-builder/", import.meta.url),
new URL("../../../tests/conformance/interface-builder/", import.meta.url),
);
try {
@@ -10,7 +10,7 @@ import {
artifactCli,
artifactMcpResult,
withArtifactMcp,
} from "./lib/artifact-e2e.mjs";
} from "../../lib/artifact-e2e.mjs";
if (process.platform !== "darwin")
throw new Error(
@@ -25,7 +25,7 @@ try {
join(root, "modules"),
fileURLToPath(
new URL(
"../tests/conformance/native/keyed-archive.swift",
"../../../tests/conformance/native/keyed-archive.swift",
import.meta.url,
),
),
@@ -66,7 +66,7 @@ try {
const expected = JSON.parse(
await readFile(
new URL(
"../tests/fixtures/golden/keyed-archive/graph.json",
"../../../tests/fixtures/golden/keyed-archive/graph.json",
import.meta.url,
),
"utf8",
@@ -124,7 +124,7 @@ try {
join(root, "modules"),
fileURLToPath(
new URL(
"../tests/conformance/native/keyed-archive-uid-root.swift",
"../../../tests/conformance/native/keyed-archive-uid-root.swift",
import.meta.url,
),
),
@@ -9,11 +9,11 @@ import {
artifactCliEvidence,
artifactMcpResult,
withArtifactMcp,
} from "./lib/artifact-e2e.mjs";
} from "../../lib/artifact-e2e.mjs";
import {
buildMacosBundleFixture,
preflightMacosBundleFixture,
} from "./lib/macos-bundle-fixture.mjs";
} from "../../fixtures/apple/macos-bundle.mjs";
const exec = promisify(execFile);
+2 -2
View File
@@ -8,13 +8,13 @@
"url": "https://github.com/morluto/rea",
"source": "github"
},
"version": "4.1.0",
"version": "5.0.0",
"packages": [
{
"registryType": "npm",
"registryBaseUrl": "https://registry.npmjs.org",
"identifier": "rea-agents",
"version": "4.1.0",
"version": "5.0.0",
"runtimeHint": "npx",
"transport": {
"type": "stdio"
+2 -2
View File
@@ -3,8 +3,8 @@ name: reverse-engineer-anything
description: Reverse engineer native, managed, Electron/JavaScript, packaged, firmware, and browser targets with REA. Use shipped-artifact or requested runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
metadata:
version: "27"
tool_count: 131
catalog_digest: "54ac160db3b96580a9a0203a07fe3db01ffc05224db621f48b4abdf979e4f50b"
tool_count: 133
catalog_digest: "83b4c3445ed0ec003c0aeefa3188f123d9ae319484f8b1e2b611d2fe278c9c72"
---
# REA
+1 -1
View File
@@ -1,7 +1,7 @@
import type { BinaryTarget } from "../domain/binaryTarget.js";
import type { OfficialToolName } from "../contracts/officialToolContracts.js";
import type { EnhancedToolName } from "../contracts/enhancedInputs.js";
import type { NativeToolName } from "../contracts/nativeToolContracts.js";
import type { NativeToolName } from "../contracts/native/nativeToolContracts.js";
import type { ArtifactAnalysisOperation } from "../contracts/artifactToolContracts.js";
import type { ManagedToolName } from "../contracts/managed/managedToolContracts.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
+6
View File
@@ -189,6 +189,12 @@ export const buildCapabilityInventory = (
};
const availabilityFor = (context: AvailabilityContext): Availability => {
// These built-in workflows accept a caller-selected endpoint; they do not load the optional passive provider.
if (
context.name === "observe_web_execution" ||
context.name === "inspect_web_event_listeners"
)
return { reason: "available", remediation: null };
if (context.name === "get_navigation_context")
return navigationContextAvailability(context.descriptors);
const javascriptApplication = javascriptApplicationAvailability(context);
+24 -18
View File
@@ -3,11 +3,12 @@ import { parseConfig } from "../config.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { EnhancedTools } from "./EnhancedTools.js";
import { executeFunctionAnalysisEvidence } from "./FunctionAnalysisEvidence.js";
import { createBinarySession, createManagedBinarySession } from "./runtime.js";
import type { DirectAnalysisDependencies } from "./DirectAnalysisDependencies.js";
import type { BinarySession } from "./binary/BinarySession.js";
import { silentLogger, type Logger } from "../logger.js";
import { createEvidence } from "../domain/evidence.js";
import type { Evidence } from "../domain/evidence.js";
import type { NativeToolName } from "../contracts/nativeToolContracts.js";
import type { NativeToolName } from "../contracts/native/nativeToolContracts.js";
import type { ArtifactAnalysisOperation } from "../contracts/artifactToolContracts.js";
import {
isManagedToolName,
@@ -38,11 +39,6 @@ import {
import type { AnalysisProviderSelector } from "../contracts/providerSelection.js";
import { artifactInspectionResultSchema } from "../domain/artifactInspection.js";
export {
runCapabilityStatus,
runProviderStatus,
} from "./DirectAnalysisStatus.js";
type DirectAnalysisTool =
| "annotate_native_function"
| "inspect_native_load_image"
@@ -70,6 +66,7 @@ type DirectAnalysisTool =
* retain a target or provider client for a subsequent command.
*/
export const runDirectAnalysis = async (
dependencies: DirectAnalysisDependencies,
path: string,
tool: DirectAnalysisTool,
arguments_: Readonly<Record<string, JsonValue>>,
@@ -82,7 +79,7 @@ export const runDirectAnalysis = async (
} = {},
): Promise<JsonValue> =>
withProcessCancellation(options.signal, (signal) =>
runAnalysis(path, tool, arguments_, {
runAnalysis(dependencies, path, tool, arguments_, {
logger: options.logger ?? silentLogger,
snapshotPath: options.snapshotPath,
signal,
@@ -97,6 +94,7 @@ export const runDirectAnalysis = async (
/** Execute one provider-native semantic operation with atomic provenance. */
export const runProviderAnalysis = async (
dependencies: DirectAnalysisDependencies,
...[path, tool, arguments_, logger = silentLogger, signal]: readonly [
path: string,
tool: NativeToolName | ArtifactAnalysisOperation | ManagedToolName,
@@ -106,9 +104,9 @@ export const runProviderAnalysis = async (
]
): Promise<JsonValue> =>
isManagedToolName(tool)
? runManagedProviderAnalysis(path, tool, signal)
? runManagedProviderAnalysis(dependencies, path, tool, signal)
: withProcessCancellation(signal, (operationSignal) =>
runAnalysis(path, tool, arguments_, {
runAnalysis(dependencies, path, tool, arguments_, {
logger,
snapshotPath: undefined,
signal: operationSignal,
@@ -117,12 +115,13 @@ export const runProviderAnalysis = async (
/** Execute managed metadata inspection in an isolated managed-only session. */
export const runManagedProviderExecution = async (
dependencies: DirectAnalysisDependencies,
path: string,
tool: ManagedToolName,
signal?: AbortSignal,
): Promise<Result<AnalysisExecution, AnalysisError>> =>
withProcessCancellation(signal, async (operationSignal) => {
const session = createManagedBinarySession();
const session = dependencies.createManagedBinarySession();
try {
const opened = await session.open(path, { signal: operationSignal });
if (!opened.ok) return opened;
@@ -133,11 +132,17 @@ export const runManagedProviderExecution = async (
});
const runManagedProviderAnalysis = async (
dependencies: DirectAnalysisDependencies,
path: string,
tool: ManagedToolName,
signal?: AbortSignal,
): Promise<JsonValue> => {
const execution = await runManagedProviderExecution(path, tool, signal);
const execution = await runManagedProviderExecution(
dependencies,
path,
tool,
signal,
);
if (!execution.ok) return cliError(execution.error);
const value = execution.value;
return createEvidence(value.subject ?? undefined, value.provider, {
@@ -151,6 +156,7 @@ const runManagedProviderAnalysis = async (
};
const runAnalysis = async (
dependencies: DirectAnalysisDependencies,
path: string,
tool:
| NativeToolName
@@ -177,7 +183,7 @@ const runAnalysis = async (
const { logger, signal, snapshotPath } = options;
const config = parseConfig(options.environment ?? process.env);
if (!config.ok) return cliError(config.error);
const session = createBinarySession(config.value, logger);
const session = dependencies.createBinarySession(config.value, logger);
try {
const prepared = await prepareSnapshot({
path,
@@ -247,13 +253,13 @@ const runAnalysis = async (
};
const canReplayDirectTool = (
session: ReturnType<typeof createBinarySession>,
tool: Parameters<typeof runAnalysis>[1],
session: BinarySession,
tool: Parameters<typeof runAnalysis>[2],
): boolean =>
tool !== "trace_native_ui_action" && session.allowsSnapshotReplay(tool);
const executeAnalysisTool = async (input: {
readonly session: ReturnType<typeof createBinarySession>;
readonly session: BinarySession;
readonly openedTarget: BinaryTarget;
readonly tool:
| NativeToolName
@@ -372,7 +378,7 @@ const prepareSnapshot = async (options: {
};
const replayProviderFor = (
session: ReturnType<typeof createBinarySession>,
session: BinarySession,
tool:
| NativeToolName
| ArtifactAnalysisOperation
@@ -384,7 +390,7 @@ const replayProviderFor = (
: session.providerIdentity(tool);
const analysisProfileForEvidence = (
session: ReturnType<typeof createBinarySession>,
session: BinarySession,
tool:
| NativeToolName
| ArtifactAnalysisOperation
@@ -0,0 +1,12 @@
import type { AppConfig } from "../config.js";
import type { Logger } from "../logger.js";
import type { BinarySession } from "./binary/BinarySession.js";
/** Existing one-shot session factories supplied by the production boundary. */
export interface DirectAnalysisDependencies {
readonly createBinarySession: (
config: AppConfig,
logger: Logger,
) => BinarySession;
readonly createManagedBinarySession: () => BinarySession;
}
+7 -4
View File
@@ -3,15 +3,16 @@ import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { jsonObjectSchema, type JsonValue } from "../domain/jsonValue.js";
import { createServerIdentity } from "../serverIdentity.js";
import { silentLogger, type Logger } from "../logger.js";
import { createBinarySession } from "./runtime.js";
import type { DirectAnalysisDependencies } from "./DirectAnalysisDependencies.js";
const runSessionStatus = async (
dependencies: Pick<DirectAnalysisDependencies, "createBinarySession">,
logger: Logger = silentLogger,
environment: Readonly<Record<string, string | undefined>> = process.env,
): Promise<JsonValue> => {
const config = parseConfig(environment);
if (!config.ok) return { error: projectAnalysisError(config.error) };
const session = createBinarySession(config.value, logger);
const session = dependencies.createBinarySession(config.value, logger);
try {
return {
...jsonObjectSchema.parse(session.status()),
@@ -26,12 +27,14 @@ const runSessionStatus = async (
/** List binary-session provider candidates and auxiliary operation availability. */
export const runProviderStatus = (
dependencies: Pick<DirectAnalysisDependencies, "createBinarySession">,
logger: Logger = silentLogger,
environment: Readonly<Record<string, string | undefined>> = process.env,
) => runSessionStatus(logger, environment);
) => runSessionStatus(dependencies, logger, environment);
/** List binary-session operation descriptors; this is not the full MCP catalog. */
export const runCapabilityStatus = (
dependencies: Pick<DirectAnalysisDependencies, "createBinarySession">,
logger: Logger = silentLogger,
environment: Readonly<Record<string, string | undefined>> = process.env,
) => runSessionStatus(logger, environment);
) => runSessionStatus(dependencies, logger, environment);
+1 -1
View File
@@ -22,7 +22,7 @@ describe("doctor", () => {
),
).toEqual({
node: `Install ${SUPPORTED_NODE_VERSION_PROSE}.`,
host: "REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and the experimental Windows x64 Ghidra P0 boundary.",
host: "REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, CachyOS, and the experimental Windows x64 Ghidra P0 boundary.",
hopper: "Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH.",
target: "Supply a readable local app or program path.",
});
+1 -1
View File
@@ -115,7 +115,7 @@ const hostCheck = async (host: DoctorHost): Promise<DoctorCheck> => {
: `${linuxDistribution.id} ${linuxDistribution.versionId ?? "unknown"}`);
return check("host", supported, detail, {
remediation:
"REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and the experimental Windows x64 Ghidra P0 boundary.",
"REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, CachyOS, and the experimental Windows x64 Ghidra P0 boundary.",
classification: "unsupported_host",
});
};
+4 -4
View File
@@ -27,9 +27,9 @@ import {
discoverObjcClasses,
discoverObjcProtocols,
} from "../domain/symbolAnalysis.js";
import { inspectNativeDispatch } from "./NativeDispatchMetadataInspection.js";
import { traceNativeValues } from "./NativeValueTrace.js";
import { traceNativeUiAction } from "./NativeUiActionTrace.js";
import { inspectNativeDispatch } from "./native/NativeDispatchMetadataInspection.js";
import { traceNativeValues } from "./native/NativeValueTrace.js";
import { traceNativeUiAction } from "./native/NativeUiActionTrace.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import {
@@ -39,7 +39,7 @@ import {
} from "./EnhancedToolTypes.js";
import { traceCallPath } from "./CallPathTracing.js";
import { traceLiteralFeature } from "./EnhancedLiteralTracing.js";
import { projectNativeApiInspection } from "./NativeApiInspection.js";
import { projectNativeApiInspection } from "./native/NativeApiInspection.js";
export type { ValidatedEnhancedCall } from "./EnhancedToolTypes.js";
/**
+122
View File
@@ -0,0 +1,122 @@
import { fstatSync } from "node:fs";
import {
mkdtemp,
lstat,
open,
readdir,
rm,
utimes,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, it } from "vitest";
import { hashFile } from "./FilesystemSnapshot.js";
const countOpenDescriptorsFor = async (path: string): Promise<number> => {
const directory = process.platform === "linux" ? "/proc/self/fd" : "/dev/fd";
const expected = await lstat(path);
const descriptors = await readdir(directory);
const matches = await Promise.all(
descriptors.map(async (descriptor) => {
if (!/^\d+$/u.test(descriptor)) return false;
try {
const stats = fstatSync(Number(descriptor));
return stats.dev === expected.dev && stats.ino === expected.ino;
} catch (cause: unknown) {
if (cause instanceof Error && "code" in cause && cause.code === "EBADF")
return false;
throw cause;
}
}),
);
return matches.filter(Boolean).length;
};
it("does not hash a replacement file using the earlier path metadata", async () => {
const directory = await mkdtemp(join(tmpdir(), "rea-snapshot-identity-"));
const path = join(directory, "observed-file");
try {
await writeFile(path, "captured file\n");
const expected = await lstat(path);
await rm(path);
await writeFile(path, "replacement with a different identity and size\n");
await expect(hashFile(path, expected, 1_000)).resolves.toBeNull();
} finally {
await rm(directory, { recursive: true, force: true });
}
});
it("rejects a same-size rewrite even when its modification time is restored", async () => {
const directory = await mkdtemp(join(tmpdir(), "rea-snapshot-ctime-"));
const path = join(directory, "observed-file");
try {
const fixedMtime = new Date("2020-01-02T03:04:05.000Z");
await writeFile(path, "before\n");
await utimes(path, fixedMtime, fixedMtime);
const expected = await lstat(path);
await writeFile(path, "after!\n");
await utimes(path, fixedMtime, fixedMtime);
const rewritten = await lstat(path);
expect(rewritten).toMatchObject({
dev: expected.dev,
ino: expected.ino,
mode: expected.mode,
size: expected.size,
mtimeMs: expected.mtimeMs,
});
expect(rewritten.ctimeMs).not.toBe(expected.ctimeMs);
await expect(hashFile(path, expected, 1_000)).resolves.toBeNull();
} finally {
await rm(directory, { recursive: true, force: true });
}
});
it("rejects cancellation for an empty file before returning its digest", async () => {
const directory = await mkdtemp(join(tmpdir(), "rea-snapshot-empty-cancel-"));
const path = join(directory, "observed-file");
try {
await writeFile(path, "");
const expected = await lstat(path);
const controller = new AbortController();
controller.abort();
await expect(
hashFile(path, expected, 1_000, controller.signal),
).rejects.toMatchObject({ name: "AbortError" });
} finally {
await rm(directory, { recursive: true, force: true });
}
});
it.skipIf(process.platform === "win32")(
"closes the opened file when cancellation arrives during open",
async () => {
const directory = await mkdtemp(join(tmpdir(), "rea-snapshot-cancel-"));
const path = join(directory, "observed-file");
try {
await writeFile(path, "captured file\n");
const expected = await lstat(path);
const controller = new AbortController();
const held = await open(path, "r");
expect(await countOpenDescriptorsFor(path)).toBe(1);
await held.close();
expect(await countOpenDescriptorsFor(path)).toBe(0);
const hashing = hashFile(path, expected, 1_000, controller.signal);
// hashFile reaches `open` synchronously before its first suspension, so
// this abort is observed after the descriptor is acquired and is handled
// by the `finally` close path.
controller.abort();
await expect(hashing).rejects.toMatchObject({ name: "AbortError" });
expect(await countOpenDescriptorsFor(path)).toBe(0);
} finally {
await rm(directory, { recursive: true, force: true });
}
},
);
+24 -6
View File
@@ -19,6 +19,16 @@ const hasSameIdentity = (
before.ino === after.ino &&
(expected === "directory" ? after.isDirectory() : after.isSymbolicLink());
const hasSameFileState = (before: Stats, after: Stats): boolean =>
before.dev === after.dev &&
before.ino === after.ino &&
before.isFile() &&
after.isFile() &&
before.mode === after.mode &&
before.size === after.size &&
before.mtimeMs === after.mtimeMs &&
before.ctimeMs === after.ctimeMs;
const lstatIfPresent = async (path: string): Promise<Stats | undefined> => {
try {
return await lstat(path);
@@ -29,18 +39,26 @@ const lstatIfPresent = async (path: string): Promise<Stats | undefined> => {
}
};
const hashFile = async (
/** Hash a file only when its opened descriptor still matches the captured path state. */
export const hashFile = async (
path: string,
expected: Stats,
maxBytes: number,
signal?: AbortSignal,
): Promise<string | null> => {
signal?.throwIfAborted();
const handle = await open(
path,
fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0),
fsConstants.O_RDONLY |
(fsConstants.O_NOFOLLOW ?? 0) |
(fsConstants.O_NONBLOCK ?? 0),
);
try {
signal?.throwIfAborted();
const stats = await handle.stat();
if (!stats.isFile() || stats.size > maxBytes) return null;
signal?.throwIfAborted();
if (!hasSameFileState(expected, stats) || stats.size > maxBytes)
return null;
const hash = createHash("sha256");
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes));
let position = 0;
@@ -57,8 +75,8 @@ const hashFile = async (
position += bytesRead;
}
const after = await handle.stat();
if (after.size !== stats.size || after.mtimeMs !== stats.mtimeMs)
return null;
signal?.throwIfAborted();
if (!hasSameFileState(stats, after)) return null;
return hash.digest("hex");
} finally {
await handle.close();
@@ -110,7 +128,7 @@ export const snapshotRoots = async (
if (stats.isFile()) {
const sha256 =
remainingBytes >= stats.size
? await hashFile(path, remainingBytes, signal)
? await hashFile(path, stats, remainingBytes, signal)
: null;
remainingBytes -= sha256 === null ? 0 : stats.size;
if (sha256 === null) truncated = true;
+3
View File
@@ -59,6 +59,7 @@ describe("Linux Hopper host classification", () => {
['ID=ubuntu\nVERSION_ID="24.04"\n', "deb"],
["ID=fedora\nVERSION_ID=41\n", "rpm"],
["ID=arch\n", "arch"],
["ID=cachyos\nID_LIKE=arch\n", "arch"],
] as const)("accepts an official Hopper distribution", (document, family) => {
expect(parseLinuxDistribution(document)).toMatchObject({
packageFamily: family,
@@ -88,6 +89,8 @@ describe("Linux Hopper host classification", () => {
'ID=ubuntu\nVERSION_ID="22.04"\n',
"ID=fedora\nVERSION_ID=40\n",
'ID=debian\nVERSION_ID="13"\nID_LIKE=debian\n',
"ID=manjaro\nID_LIKE=arch\n",
"ID=garuda\nID_LIKE=arch\n",
])("rejects unsupported vendor/version combinations", (document) => {
expect(parseLinuxDistribution(document).supported).toBe(false);
});
+1
View File
@@ -150,6 +150,7 @@ export const parseLinuxDistribution = (text: string): LinuxDistribution => {
const supported =
packageFamily !== undefined &&
(id === "arch" ||
id === "cachyos" ||
(id === "ubuntu" && versionAtLeast(versionId, 24)) ||
(id === "fedora" && versionAtLeast(versionId, 41)));
const identity = {
+2 -1
View File
@@ -449,6 +449,7 @@ export const resolveProcessResult = (
export const prepareProcessCapture = async (
scenario: ProcessScenario,
signal: AbortSignal | undefined,
captureSnapshot: typeof snapshotRoots = snapshotRoots,
host: ProcessPreparationHost = systemProcessPreparationHost,
): Promise<{
readonly temporaryRoot: string;
@@ -456,7 +457,7 @@ export const prepareProcessCapture = async (
readonly before: SnapshotResult;
}> => {
assertNotCancelled(signal);
const before = await snapshotRoots(scenario, signal);
const before = await captureSnapshot(scenario, signal);
const temporaryRoot = await host.createTemporaryRoot();
try {
const runId = randomUUID();
+5 -1
View File
@@ -165,7 +165,11 @@ const readJson = async (path: string): Promise<unknown> => {
);
}
try {
const parsed: unknown = JSON.parse(bytes.toString("utf8"));
const text = new TextDecoder("utf-8", {
fatal: true,
ignoreBOM: true,
}).decode(bytes);
const parsed: unknown = JSON.parse(text);
return parsed;
} catch (cause: unknown) {
void cause;
+31 -5
View File
@@ -233,6 +233,20 @@ const finishProcessRun = async (options: {
);
};
const normalizeCaptureFailure = (
cause: unknown,
signal: AbortSignal | undefined,
): unknown => {
if (cause instanceof ProcessCaptureError) return cause;
if (
signal?.aborted === true &&
(cause === signal.reason ||
(cause instanceof Error && cause.name === "AbortError"))
)
return processCaptureCancelled();
return cause;
};
const completeCapture = async (options: {
readonly scenario: ProcessScenario;
readonly hostPlatform: NodeJS.Platform;
@@ -245,6 +259,7 @@ const completeCapture = async (options: {
readonly interactions: readonly InteractionEvent[];
readonly exit: Awaited<ReturnType<typeof awaitTerminalExit>>;
readonly signal?: AbortSignal;
readonly captureSnapshot: typeof snapshotRoots;
readonly initiallyTruncated: boolean;
readonly eventJournal: readonly ProcessCaptureEventJournalEntry[];
readonly recordEvent: RecordProcessCaptureEvent;
@@ -262,7 +277,7 @@ const completeCapture = async (options: {
const samplingPartial = (await runtime.stopSampler()).partial;
await settleProcessCaptureJournal(options.eventJournal);
assertNotCancelled(options.signal);
const after = await snapshotRoots(scenario, options.signal);
const after = await options.captureSnapshot(scenario, options.signal);
options.recordEvent("filesystem_checkpoints", 1);
const renderedFrames = await runtime.renderer.frames();
const checkpoints: UnverifiedProcessCapture["filesystem_checkpoints"] = [
@@ -319,10 +334,12 @@ const runProcessScenario = async (
signal?: AbortSignal,
hostEnvironment: Readonly<Record<string, string | undefined>> = process.env,
hostPlatform: NodeJS.Platform = process.platform,
captureSnapshot: typeof snapshotRoots = snapshotRoots,
): Promise<ProcessCapture> => {
const { temporaryRoot, runId, before } = await prepareProcessCapture(
scenario,
signal,
captureSnapshot,
);
const frames: TerminalFrame[] = [];
const samples: ProcessSample[] = [];
@@ -377,11 +394,12 @@ const runProcessScenario = async (
initiallyTruncated: before.truncated,
eventJournal,
recordEvent,
captureSnapshot,
...(signal === undefined ? {} : { signal }),
});
} catch (cause: unknown) {
runtime?.terminal.kill("SIGKILL");
executionFailure = cause;
executionFailure = normalizeCaptureFailure(cause, signal);
}
return finishProcessRun({
runtime,
@@ -401,6 +419,7 @@ export const captureProcessScenario = async (
signal?: AbortSignal,
platform: NodeJS.Platform = process.platform,
environment: Readonly<Record<string, string | undefined>> = process.env,
captureSnapshot: typeof snapshotRoots = snapshotRoots,
): Promise<Result<ProcessCapture, ProcessCaptureError | AnalysisError>> => {
const ownershipReason = processCaptureOwnershipUnavailableReason(platform);
if (ownershipReason !== undefined)
@@ -419,12 +438,19 @@ export const captureProcessScenario = async (
environment,
);
return ok(
await runProcessScenario(resolvedScenario, signal, environment, platform),
await runProcessScenario(
resolvedScenario,
signal,
environment,
platform,
captureSnapshot,
),
);
} catch (cause: unknown) {
const failure = normalizeCaptureFailure(cause, signal);
return err(
cause instanceof ProcessCaptureError
? cause
failure instanceof ProcessCaptureError
? failure
: new ProcessCaptureError("process capture failed", { cause }),
);
}
+1 -1
View File
@@ -194,7 +194,7 @@ describe("setup scoped readiness", () => {
expect(result.status).toBe("needs_human");
expect(host.hopperInstalls).toBe(0);
expect(result.remediation).toBe(
"Automated Hopper setup supports Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux; configure an existing supported provider instead.",
"Automated Hopper setup supports Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and CachyOS; configure an existing supported provider instead.",
);
});
+1 -1
View File
@@ -82,7 +82,7 @@ export const hostRemediation = async (
: undefined;
}
if ((await host.linuxDistribution())?.supported === true) return undefined;
return "Automated Hopper setup supports Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux; configure an existing supported provider instead.";
return "Automated Hopper setup supports Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and CachyOS; configure an existing supported provider instead.";
};
/** Production setup effects for Hopper, agent configuration, and the canonical skill directory. */
+24
View File
@@ -0,0 +1,24 @@
import type { ExecutionOptions, ProviderIdentity } from "./AnalysisProvider.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { Result } from "../domain/result.js";
import type {
ObserveWebExecutionInput,
WebExecution,
} from "../domain/webExecution.js";
import type {
InspectWebEventListenersInput,
WebEventListeners,
} from "../domain/webEventListeners.js";
/** Provider-neutral live source attribution operations with distinct instrumentation authority. */
export interface WebRuntimePort {
identity(): ProviderIdentity;
observeExecution(
input: ObserveWebExecutionInput,
options?: ExecutionOptions,
): Promise<Result<WebExecution, AnalysisError>>;
inspectEventListeners(
input: InspectWebEventListenersInput,
options?: ExecutionOptions,
): Promise<Result<WebEventListeners, AnalysisError>>;
}
@@ -0,0 +1,90 @@
import type { WebRuntimePort } from "./WebRuntimePort.js";
import { ok } from "../domain/result.js";
import type { WebExecution } from "../domain/webExecution.js";
import type { WebEventListeners } from "../domain/webEventListeners.js";
const browser = {
product: "RecordingChrome/1",
protocol_version: "1.3",
revision: "fixture",
user_agent: "fixture",
js_version: "fixture",
};
const target = {
target_id: "selected-page",
initial_url: "http://127.0.0.1:9222/app",
origin: "http://127.0.0.1:9222",
frame_id: "main-frame",
loader_id: "fixture-document",
};
const at = "2026-01-01T00:00:00.000Z";
/** Immutable application-port result; it makes no real browser or socket claim. */
export const webExecutionFixture = (
targetId = target.target_id,
duration = 5,
): WebExecution => ({
browser,
target: { ...target, target_id: targetId },
window: {
armed_at: at,
ended_at: at,
requested_ms: duration,
end_reason: "window_elapsed",
producer_started_seconds: 1,
producer_sampled_seconds: 2,
},
coverage: {
state: "captured",
reason: null,
offset_units: "utf16-code-units",
end_offset: "exclusive",
scripts: [],
excluded_scripts: 0,
},
requests: [],
excluded_requests: 0,
sources: [],
script_inventory: {
main_document_scripts: 0,
not_reported_script_ids: [],
coverage_absence: "unknown",
},
instrumentation: {
resets_execution_counters: true,
disables_optimized_execution: true,
takes_one_resetting_sample: true,
executes_selected_code: false,
cleanup: "confirmed",
page_ownership: "external",
},
limitations: ["Recording port fixture, not real browser evidence."],
});
/** Immutable listener port result for application and SDK schema boundaries. */
export const webListenersFixture = (
targetId = target.target_id,
selector = "#selected",
): WebEventListeners => ({
browser,
target: { ...target, target_id: targetId },
inspected_at: at,
selected_node: { selector, match: "found", backend_node_id: 22 },
listeners: [],
sources: [],
cleanup: "confirmed",
limitations: ["Recording port fixture, not real listener evidence."],
});
/** Provider-neutral recording seam preserves the exact parsed caller target and window. */
export const recordingWebRuntimePort = (): WebRuntimePort => ({
identity: () => ({
id: "runtime-recording",
name: "Runtime recording fixture",
version: "1",
}),
observeExecution: async (input) =>
ok(webExecutionFixture(input.target_id, input.observation_ms)),
inspectEventListeners: async (input) =>
ok(webListenersFixture(input.target_id, input.selector)),
});
+61
View File
@@ -0,0 +1,61 @@
import { expect, it } from "vitest";
import { WebRuntimeService } from "./WebRuntimeService.js";
import {
recordingWebRuntimePort,
webListenersFixture,
} from "./WebRuntimeService.fixture.js";
import { err, ok } from "../domain/result.js";
import { AnalysisTimeoutError } from "../domain/analysisErrorCore.js";
it("rejects malformed input and prior cancellation without calling a provider", async () => {
const provider = recordingWebRuntimePort();
const service = new WebRuntimeService({
identity: () => provider.identity(),
observeExecution: () => {
throw new Error("Provider must not be called");
},
inspectEventListeners: () => {
throw new Error("Provider must not be called");
},
});
const malformed = await service.observe({
cdp_endpoint: "https://remote.example",
target_id: "page",
});
if (malformed.ok)
throw new Error("Invalid endpoint must fail before execution");
expect(malformed.error._tag).toBe("AnalysisInputError");
const cancelled = await service.inspect({}, { signal: AbortSignal.abort() });
if (cancelled.ok)
throw new Error("Prior cancellation must fail before inspection");
expect(cancelled.error._tag).toBe("AnalysisCancelledError");
});
it("preserves actual provider failures and rejects changed target/selector identities", async () => {
const provider = recordingWebRuntimePort();
const args = {
cdp_endpoint: "http://127.0.0.1:9222",
target_id: "selected-page",
selector: "#selected",
};
const captured = webListenersFixture();
const timeout = new AnalysisTimeoutError("observe_web_execution", 20_000);
const service = new WebRuntimeService({
identity: () => provider.identity(),
observeExecution: async () => err(timeout),
inspectEventListeners: async () =>
ok({
...captured,
selected_node: { ...captured.selected_node, selector: "#different" },
}),
});
const failed = await service.observe({
cdp_endpoint: args.cdp_endpoint,
target_id: args.target_id,
observation_ms: 5,
});
expect(failed).toEqual(err(timeout));
const changed = await service.inspect(args);
if (changed.ok) throw new Error("Changed selection must be rejected");
expect(changed.error._tag).toBe("AnalysisOutputError");
});
+146
View File
@@ -0,0 +1,146 @@
import { z } from "zod";
import type { ExecutionOptions } from "./AnalysisProvider.js";
import type { WebRuntimePort } from "./WebRuntimePort.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
import { createEvidence, type Evidence } from "../domain/evidence.js";
import { jsonObjectSchema, jsonValueSchema } from "../domain/jsonValue.js";
import { projectInputIssues } from "../domain/inputIssueProjection.js";
import { err, ok, type Result } from "../domain/result.js";
import {
observeWebExecutionInputSchema,
webExecutionSchema,
} from "../domain/webExecution.js";
import {
inspectWebEventListenersInputSchema,
webEventListenersSchema,
} from "../domain/webEventListeners.js";
/** Shared public workflows keep instrumentation and listener inspection independently selectable. */
export class WebRuntimeService {
constructor(readonly provider: WebRuntimePort) {}
/** Observe externally triggered execution; selection authorizes the declared instrumentation. */
async observe(
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const operation = "observe_web_execution";
const parsed = parseInput(
observeWebExecutionInputSchema,
rawInput,
operation,
options,
);
if (!parsed.ok) return parsed;
const result = await this.provider.observeExecution(parsed.value, options);
if (!result.ok) return result;
const verified = webExecutionSchema.safeParse(result.value);
if (!verified.success)
return err(new AnalysisOutputError(operation, verified.error.message));
if (
verified.data.target.target_id !== parsed.value.target_id ||
verified.data.window.requested_ms !== parsed.value.observation_ms
)
return err(
new AnalysisOutputError(
operation,
"Provider changed the selected target or requested window.",
),
);
return this.evidence(operation, parsed.value, verified.data, options);
}
/** Inspect one selected node's listener declarations without dispatching an event. */
async inspect(
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const operation = "inspect_web_event_listeners";
const parsed = parseInput(
inspectWebEventListenersInputSchema,
rawInput,
operation,
options,
);
if (!parsed.ok) return parsed;
const result = await this.provider.inspectEventListeners(
parsed.value,
options,
);
if (!result.ok) return result;
const verified = webEventListenersSchema.safeParse(result.value);
if (!verified.success)
return err(new AnalysisOutputError(operation, verified.error.message));
if (
verified.data.target.target_id !== parsed.value.target_id ||
verified.data.selected_node.selector !== parsed.value.selector
)
return err(
new AnalysisOutputError(
operation,
"Provider changed the selected target or node selector.",
),
);
return this.evidence(operation, parsed.value, verified.data, options);
}
private evidence(
operation: string,
input:
| z.infer<typeof observeWebExecutionInputSchema>
| z.infer<typeof inspectWebEventListenersInputSchema>,
result:
| z.infer<typeof webExecutionSchema>
| z.infer<typeof webEventListenersSchema>,
options?: ExecutionOptions,
): Result<Evidence, AnalysisError> {
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(operation));
if (
input.allowed_origins.length > 0 &&
!input.allowed_origins.includes(result.target.origin)
)
return err(
new AnalysisOutputError(
operation,
"Provider returned a document outside the selected origin scope.",
),
);
return ok(
createEvidence(undefined, this.provider.identity(), {
predicateType: `rea.${operation.replaceAll("_", "-")}`,
operation,
parameters: jsonObjectSchema.parse(input),
result: jsonValueSchema.parse(result),
confidence: "observed",
authority: "external-service",
limitations: result.limitations,
}),
);
}
}
const parseInput = <T>(
schema: z.ZodType<T>,
rawInput: unknown,
operation: string,
options: ExecutionOptions | undefined,
): Result<T, AnalysisError> => {
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(operation));
const parsed = schema.safeParse(rawInput);
return parsed.success
? ok(parsed.data)
: err(
new AnalysisInputError(
operation,
{ cause: parsed.error },
projectInputIssues(parsed.error.issues, rawInput),
),
);
};
@@ -1,12 +1,12 @@
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { Evidence } from "../domain/evidence.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type { Evidence } from "../../domain/evidence.js";
import {
appleApplicationProjectionInputSchema,
projectAppleApplication,
} from "../domain/appleApplication.js";
import type { Result } from "../domain/result.js";
import { APPLE_APPLICATION_PROVIDER } from "./InvestigationProviders.js";
import { projectInventoryEvidence } from "./InventoryProjectionEvidence.js";
} from "../../domain/apple/appleApplication.js";
import type { Result } from "../../domain/result.js";
import { APPLE_APPLICATION_PROVIDER } from "../InvestigationProviders.js";
import { projectInventoryEvidence } from "../InventoryProjectionEvidence.js";
const OPERATION = "project_apple_application_graph" as const;
@@ -68,7 +68,8 @@ export abstract class BinarySessionRecords {
): Result<number, EvidenceIntegrityError> {
const imported = this.#records.mergeEvidenceBundle(bundle);
if (!imported.ok) return imported;
if (imported.value.changed) this.#emitSnapshotChanged();
if (imported.value.metadataChanged) this.invalidateSnapshot();
else if (imported.value.changed) this.#emitSnapshotChanged();
return ok(imported.value.recordsAdded);
}
@@ -77,6 +77,78 @@ const update = (
});
describe("residual unknown registry reads and identity", () => {
it("merges record-only bundles without changing retained unknown history", () => {
const store = ledger();
const created = store.recordUnknown(
input("Does a later record preserve this unknown?"),
mutation("record-only-import"),
);
expect(created.ok).toBe(true);
const originalUnknowns = store.export().unknowns;
const addition = evidence("record-only-addition");
const bundle = createEvidenceBundle([addition]);
expect(store.import(bundle)).toEqual({
ok: true,
value: {
recordsAdded: 1,
unknownsAdded: 0,
changed: true,
metadataChanged: false,
},
});
expect(store.export().unknowns).toEqual(originalUnknowns);
expect(store.import(bundle)).toEqual({
ok: true,
value: {
recordsAdded: 0,
unknownsAdded: 0,
changed: false,
metadataChanged: false,
},
});
});
});
describe("residual unknown dependency histories", () => {
it("imports a valid dependency chain deeper than the JavaScript call stack", () => {
const records = [];
const unknowns: ResidualUnknown[] = [];
let previousId: string | undefined;
for (let index = 0; index < 10_000; index += 1) {
const mutationEvidence = mutation(`deep-chain-${index}`);
records.push(mutationEvidence);
const relationships =
previousId === undefined
? []
: [{ type: "depends-on" as const, unknown_id: previousId }];
const unknown = createResidualUnknown(
input(`Does chain item ${index} meet the expected state?`, {
domain: "chain-profile",
required_authority: null,
relationships,
}),
mutationEvidence.evidence_id,
null,
);
unknowns.push(unknown);
previousId = unknown.unknown_id;
}
const bundle = createEvidenceBundle(records, unknowns);
const store = ledger();
expect(store.import(bundle)).toEqual({
ok: true,
value: {
recordsAdded: 10_000,
unknownsAdded: 10_000,
changed: true,
metadataChanged: false,
},
});
expect(store.export()).toEqual(bundle);
});
it("returns detached unknowns and evidence bundles from every read surface", () => {
const store = ledger();
expect(store.record(evidence("detached-record")).ok).toBe(true);
@@ -38,9 +38,55 @@ describe("evidence ledger recording", () => {
});
expect(ledger.import(createEvidenceBundle([evidence]))).toEqual({
ok: true,
value: { recordsAdded: 0, unknownsAdded: 0, changed: false },
value: {
recordsAdded: 0,
unknownsAdded: 0,
changed: false,
metadataChanged: false,
},
});
expect(ledger.export().records).toEqual([evidence]);
const relocatedBundleRecord = createEvidence(
{ ...TARGET, path: "/relocated/fixture-renamed" },
PROVIDER,
{ operation: "health", parameters: {}, result: true },
);
expect(
ledger.import(createEvidenceBundle([relocatedBundleRecord])),
).toEqual({
ok: true,
value: {
recordsAdded: 0,
unknownsAdded: 0,
changed: true,
metadataChanged: true,
},
});
expect(ledger.get(evidence.evidence_id)).toEqual(relocatedBundleRecord);
expect(ledger.export().records).toEqual([relocatedBundleRecord]);
expect(
ledger.import(createEvidenceBundle([relocatedBundleRecord])),
).toEqual({
ok: true,
value: {
recordsAdded: 0,
unknownsAdded: 0,
changed: false,
metadataChanged: false,
},
});
const conflictingBundle = createEvidenceBundle([
evidence,
relocatedBundleRecord,
]);
expect(ledger.import(conflictingBundle).ok).toBe(false);
expect(ledger.get(evidence.evidence_id)).toEqual(relocatedBundleRecord);
ledger.clear();
expect(ledger.export().records).toEqual([]);
});
it("ignores JSON object key order during imported identity checks", () => {
const ledger = new EvidenceLedger();
const ordered = createEvidence(TARGET, PROVIDER, {
operation: "health",
parameters: { alpha: 1, beta: 2 },
@@ -60,11 +106,78 @@ describe("evidence ledger recording", () => {
);
expect(ledger.import(reorderedBundle)).toEqual({
ok: true,
value: { recordsAdded: 0, unknownsAdded: 0, changed: false },
value: {
recordsAdded: 0,
unknownsAdded: 0,
changed: false,
metadataChanged: false,
},
});
ledger.clear();
expect(ledger.export().records).toEqual([]);
});
});
describe("evidence ledger metadata imports", () => {
it("reports and commits metadata replacement when importing unknown history", () => {
const ledger = new EvidenceLedger();
const mutation = createEvidence(TARGET, PROVIDER, {
predicateType: "rea.residual-unknown-mutation",
operation: "record_unknown",
parameters: {},
result: { action: "record", question: "Which path is current?" },
});
const created = ledger.recordUnknown(
recordUnknownInputSchema.parse({
approved: true,
question: "Which path is current?",
severity: "high",
domain: "fixture",
required_authority: null,
required_confidence: "derived",
required_environment: null,
recommended_probes: [],
relationships: [],
}),
mutation,
);
expect(created.ok).toBe(true);
const relocatedMutation = createEvidence(
{ ...TARGET, path: "/relocated/mutation" },
PROVIDER,
{
predicateType: "rea.residual-unknown-mutation",
operation: "record_unknown",
parameters: {},
result: { action: "record", question: "Which path is current?" },
},
);
expect(relocatedMutation.evidence_id).toBe(mutation.evidence_id);
const bundle = createEvidenceBundle(
[relocatedMutation],
ledger.export().unknowns,
);
expect(ledger.import(bundle)).toEqual({
ok: true,
value: {
recordsAdded: 0,
unknownsAdded: 0,
changed: true,
metadataChanged: true,
},
});
expect(ledger.get(mutation.evidence_id)).toEqual(relocatedMutation);
expect(ledger.import(bundle)).toEqual({
ok: true,
value: {
recordsAdded: 0,
unknownsAdded: 0,
changed: false,
metadataChanged: false,
},
});
});
it("retains more than ten thousand inline evidence records", () => {
const ledger = new EvidenceLedger();
@@ -27,6 +27,7 @@ export interface EvidenceImportDelta {
readonly recordsAdded: number;
readonly unknownsAdded: number;
readonly changed: boolean;
readonly metadataChanged: boolean;
}
type ImportResult = Result<EvidenceImportDelta, EvidenceLedgerFailure>;
@@ -70,11 +71,50 @@ export class EvidenceLedger {
}),
);
}
// A record-only bundle cannot change the already-valid unknown graph. The
// bundle parser has validated every incoming record and its manifests, so
// checking ID conflicts and committing only the additions preserves the
// atomic merge without rebuilding and reparsing the entire ledger.
if (bundle.unknowns.length === 0) {
const additions = new Map<string, Evidence>();
const replacements = new Map<string, Evidence>();
let metadataChanged = false;
for (const evidence of bundle.records) {
const existing = this.#records.get(evidence.evidence_id);
if (existing !== undefined) {
if (!recordsAgree(existing, evidence))
return err(
new EvidenceIntegrityError("Conflicting evidence record"),
);
if (canonicalize(existing) !== canonicalize(evidence)) {
replacements.set(evidence.evidence_id, evidence);
metadataChanged = true;
}
continue;
}
additions.set(evidence.evidence_id, evidence);
}
for (const [id, evidence] of replacements)
this.#records.set(id, evidence);
for (const [id, evidence] of additions) this.#records.set(id, evidence);
return ok({
recordsAdded: additions.size,
unknownsAdded: 0,
changed: additions.size > 0 || metadataChanged,
metadataChanged,
});
}
const pending = new Map(this.#records);
let metadataChanged = false;
for (const evidence of bundle.records) {
const existing = pending.get(evidence.evidence_id);
if (existing !== undefined && !recordsAgree(existing, evidence))
return err(new EvidenceIntegrityError("Conflicting evidence record"));
if (
existing !== undefined &&
canonicalize(existing) !== canonicalize(evidence)
)
metadataChanged = true;
pending.set(evidence.evidence_id, evidence);
}
const pendingUnknowns = new Map(this.#unknownRevisions);
@@ -108,7 +148,8 @@ export class EvidenceLedger {
return ok({
recordsAdded: added,
unknownsAdded,
changed: added > 0 || unknownsAdded > 0,
changed: added > 0 || unknownsAdded > 0 || metadataChanged,
metadataChanged,
});
}
@@ -133,11 +133,21 @@ describe("investigation record ownership", () => {
).toBe(true);
expect(records.mergeEvidenceBundle(bundle)).toEqual({
ok: true,
value: { recordsAdded: 0, unknownsAdded: 1, changed: true },
value: {
recordsAdded: 0,
unknownsAdded: 1,
changed: true,
metadataChanged: false,
},
});
expect(records.mergeEvidenceBundle(bundle)).toEqual({
ok: true,
value: { recordsAdded: 0, unknownsAdded: 0, changed: false },
value: {
recordsAdded: 0,
unknownsAdded: 0,
changed: false,
metadataChanged: false,
},
});
records.clear();
expect(records.exportEvidenceBundle()).toEqual(createEvidenceBundle([]));
@@ -7,19 +7,19 @@ import {
createAnalysisExecution,
type AnalysisExecution,
type AnalysisOperation,
} from "./AnalysisProvider.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
} from "../AnalysisProvider.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
} from "../domain/analysisErrorCore.js";
import { EnhancedTools } from "./EnhancedTools.js";
} from "../../domain/analysisErrorCore.js";
import { EnhancedTools } from "../EnhancedTools.js";
import {
nativeInvestigationGraphSchema,
nativeInvestigationTraceSchema,
} from "../domain/nativeInvestigationGraph.js";
import { err, ok } from "../domain/result.js";
import type { Result } from "../domain/result.js";
} from "../../domain/native/nativeInvestigationGraph.js";
import { err, ok } from "../../domain/result.js";
import type { Result } from "../../domain/result.js";
const target = "a".repeat(64);
const provider = { id: "fixture", name: "Fixture", version: "1" };
@@ -1,8 +1,8 @@
import { describe, expect, it } from "vitest";
import { functionDossierSchema } from "../domain/hopperValues.js";
import { ghidraFunctionDossier } from "../domain/ghidraValues.fixture.js";
import { nativeApiBoundarySchema } from "../domain/nativeApiBoundary.js";
import { functionDossierSchema } from "../../domain/hopperValues.js";
import { ghidraFunctionDossier } from "../../domain/ghidraValues.fixture.js";
import { nativeApiBoundarySchema } from "../../domain/native/nativeApiBoundary.js";
import { projectNativeApiInspection } from "./NativeApiInspection.js";
describe("native API switch uncertainty", () => {
@@ -1,9 +1,9 @@
import type { FunctionDossier } from "../domain/hopperValues.js";
import type { FunctionDossier } from "../../domain/hopperValues.js";
import {
nativeApiInspectionResultSchema,
type NativeApiBoundary,
type NativeApiInspectionResult,
} from "../domain/nativeApiBoundary.js";
} from "../../domain/native/nativeApiBoundary.js";
const unavailableBoundary = (): NativeApiBoundary => ({
available: false,
@@ -1,10 +1,10 @@
import { describe, expect, it } from "vitest";
import { readNativeCallRoutes } from "./NativeCallRoutes.js";
import { createAnalysisExecution } from "./AnalysisProvider.js";
import { ghidraReferenceEdge } from "../domain/ghidraValues.fixture.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { err, ok } from "../domain/result.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { createAnalysisExecution } from "../AnalysisProvider.js";
import { ghidraReferenceEdge } from "../../domain/ghidraValues.fixture.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import { err, ok } from "../../domain/result.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
const provider = { id: "fixture", name: "Fixture", version: "1" };
describe("typed native call routes", () => {
@@ -1,11 +1,11 @@
import { z } from "zod";
import type { AnalysisOperationPort } from "./AnalysisProvider.js";
import type { AnalysisOperationPort } from "../AnalysisProvider.js";
import {
parseRelatedAddresses,
referenceKindSchema,
} from "../domain/hopperValues.js";
import { err, ok } from "../domain/result.js";
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
} from "../../domain/hopperValues.js";
import { err, ok } from "../../domain/result.js";
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
const referencesSchema = z.object({
reference_kinds_available: z.boolean().optional(),
@@ -2,12 +2,12 @@ import { z } from "zod";
import {
nativeDispatchMetadataResultSchema,
inspectNativeDispatchMetadata,
} from "../domain/objcSwiftMetadata.js";
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { err, ok } from "../domain/result.js";
import type { AnalysisOperationPort } from "./AnalysisProvider.js";
import type { EnhancedResult } from "./EnhancedToolTypes.js";
} from "../../domain/native/objcSwiftMetadata.js";
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import { err, ok } from "../../domain/result.js";
import type { AnalysisOperationPort } from "../AnalysisProvider.js";
import type { EnhancedResult } from "../EnhancedToolTypes.js";
/** Prefer validated binary metadata, otherwise preserve the bound provider's symbol-only coverage. */
export const inspectNativeDispatch = async (
analysis: AnalysisOperationPort,
@@ -1,22 +1,22 @@
import { z } from "zod";
import type { AnalysisOperationPort } from "./AnalysisProvider.js";
import type { EnhancedResult } from "./EnhancedToolTypes.js";
import type { enhancedInputSchemas } from "../contracts/enhancedInputs.js";
import { interfaceBuilderAnalysisSchema } from "../domain/interfaceBuilderGraph.js";
import type { AnalysisOperationPort } from "../AnalysisProvider.js";
import type { EnhancedResult } from "../EnhancedToolTypes.js";
import type { enhancedInputSchemas } from "../../contracts/enhancedInputs.js";
import { interfaceBuilderAnalysisSchema } from "../../domain/apple/interfaceBuilderGraph.js";
import {
nativeInvestigationGraphSchema,
nativeInvestigationTraceSchema,
traceNativeInvestigationGraph,
joinInterfaceBuilderDispatch,
} from "../domain/nativeInvestigationGraph.js";
import { nativeDispatchMetadataResultSchema } from "../domain/objcSwiftMetadata.js";
} from "../../domain/native/nativeInvestigationGraph.js";
import { nativeDispatchMetadataResultSchema } from "../../domain/native/objcSwiftMetadata.js";
import { inspectNativeDispatch } from "./NativeDispatchMetadataInspection.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import { readNativeCallRoutes } from "./NativeCallRoutes.js";
import { err, ok } from "../domain/result.js";
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { err, ok } from "../../domain/result.js";
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
type Input = z.output<typeof enhancedInputSchemas.trace_native_ui_action>;
/** Build a UI-to-code trace from the active app and selected native provider. */
@@ -3,12 +3,12 @@ import { traceNativeValues } from "./NativeValueTrace.js";
import {
createAnalysisExecution,
type AnalysisOperationPort,
} from "./AnalysisProvider.js";
import { ghidraFunctionDossier } from "../domain/ghidraValues.fixture.js";
import { functionDossierSchema } from "../domain/hopperValues.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { ok, err } from "../domain/result.js";
} from "../AnalysisProvider.js";
import { ghidraFunctionDossier } from "../../domain/ghidraValues.fixture.js";
import { functionDossierSchema } from "../../domain/hopperValues.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import { ok, err } from "../../domain/result.js";
const subject = {
path: "/fixture",
@@ -1,17 +1,17 @@
import { functionDossierSchema } from "../domain/hopperValues.js";
import { nativeCallTargetsSchema } from "../domain/nativeInstruction.js";
import { functionDossierSchema } from "../../domain/hopperValues.js";
import { nativeCallTargetsSchema } from "../../domain/native/nativeInstruction.js";
import {
nativeValueTraceInputSchema,
nativeValueTraceSchema,
} from "../domain/nativeValueTrace.js";
import { createEvidence } from "../domain/evidence.js";
} from "../../domain/native/nativeValueTrace.js";
import { createEvidence } from "../../domain/evidence.js";
import {
AnalysisCancelledError,
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
import { err, ok } from "../domain/result.js";
import type { AnalysisOperationPort } from "./AnalysisProvider.js";
import type { EnhancedResult } from "./EnhancedToolTypes.js";
} from "../../domain/analysisErrorCore.js";
import { err, ok } from "../../domain/result.js";
import type { AnalysisOperationPort } from "../AnalysisProvider.js";
import type { EnhancedResult } from "../EnhancedToolTypes.js";
/** Compose bounded high-p-code def-use graphs across exactly referenced call destinations. */
export const traceNativeValues = async (
+4 -4
View File
@@ -7,12 +7,12 @@ import {
type ProviderIdentity,
type ExecutionOptions,
} from "../application/AnalysisProvider.js";
import { inspectBundleKeyedArchive } from "./KeyedArchiveReader.js";
import { inspectBundleKeyedArchive } from "./apple/KeyedArchiveReader.js";
import { basename, dirname } from "node:path";
import { inventoryArtifact } from "../application/ArtifactInventory.js";
import { extractArtifact } from "../application/ArtifactExtraction.js";
import { analyzeInterfaceBuilderBundle } from "../application/InterfaceBuilderAnalysis.js";
import { analyzeAppleAssetCatalogs } from "../application/AppleAssetCatalogAnalysis.js";
import { analyzeInterfaceBuilderBundle } from "./apple/InterfaceBuilderAnalysis.js";
import { analyzeAppleAssetCatalogs } from "./apple/AppleAssetCatalogAnalysis.js";
import {
ARTIFACT_ANALYSIS_OPERATIONS,
artifactInventoryInputSchema,
@@ -24,7 +24,7 @@ import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.
import { ArtifactOperationError } from "../domain/artifactOperationError.js";
import { type AnalysisError } from "../domain/analysisErrorBase.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { interfaceBuilderLimitsSchema } from "../domain/interfaceBuilderGraph.js";
import { interfaceBuilderLimitsSchema } from "../domain/apple/interfaceBuilderGraph.js";
import { err, ok } from "../domain/result.js";
import { ArtifactReaderFailure } from "./ArtifactReader.js";
import {
+269 -32
View File
@@ -1,17 +1,19 @@
import { createReadStream } from "node:fs";
import { sep } from "node:path";
import { constants, createReadStream, type Stats } from "node:fs";
import { lstat, open, realpath, type FileHandle } from "node:fs/promises";
import { isAbsolute, join, relative, sep } from "node:path";
import { Readable } from "node:stream";
import { extractFile, listPackage, statFile, uncache } from "@electron/asar";
import { getRawHeader, listPackage, statFile, uncache } from "@electron/asar";
import {
ArtifactReaderFailure,
type ArtifactEntry,
type ArtifactReader,
} from "./ArtifactReader.js";
import { closeAsarHandle, readValidatedAsarEntry } from "./AsarEntryStream.js";
/**
* Official Electron ASAR adapter. Individual files remain caller-bounded.
* Official Electron ASAR adapter with range-streamed member reads.
*
* An entry marked `unpacked` is metadata, not an integrity exemption: Electron
* stores its bytes beside the archive in `<archive>.unpacked`, and callers must
@@ -19,17 +21,33 @@ import {
*/
export class AsarArtifactReader implements ArtifactReader {
readonly format = "asar" as const;
#archiveSize: number | undefined;
#headerSize: number | undefined;
readonly #entries = new Map<string, AsarEntryState>();
constructor(private readonly path: string) {}
constructor(
private readonly path: string,
private readonly openUnpackedFile: OpenAsarUnpackedFile = open,
) {}
async *entries(signal?: AbortSignal): AsyncIterable<ArtifactEntry> {
let paths: string[];
try {
// ASAR caches headers by path even after the archive has been replaced.
this.#resetArchiveState();
this.#entries.clear();
uncache(this.path);
paths = listPackage(this.path, { isPack: false }).sort((left, right) =>
left.localeCompare(right, "en"),
);
this.#headerSize = getRawHeader(this.path).headerSize;
const archiveMetadata = await lstat(this.path);
if (!archiveMetadata.isFile() || archiveMetadata.isSymbolicLink())
throw new ArtifactReaderFailure(
"format",
"ASAR container is not a regular file",
);
this.#archiveSize = archiveMetadata.size;
} catch (cause: unknown) {
throw asarFailure(this.path, "inventory", cause);
}
@@ -56,7 +74,7 @@ export class AsarArtifactReader implements ArtifactReader {
: "link" in metadata
? "symlink"
: "file";
yield {
const entry: ArtifactEntry = {
path,
kind,
declaredSize: "size" in metadata ? metadata.size : null,
@@ -74,42 +92,121 @@ export class AsarArtifactReader implements ArtifactReader {
limitations:
kind === "symlink"
? ["ASAR symlink target was not followed or disclosed."]
: [
"Official ASAR extraction buffers each individually bounded file.",
],
: [],
adapterKey: providerPath,
};
if (kind === "file" && isAsarFileMetadata(metadata))
this.#entries.set(providerPath, {
metadata,
entry: copyArtifactEntry(entry),
});
yield entry;
}
}
open(entry: ArtifactEntry, signal?: AbortSignal): Promise<Readable> {
async open(entry: ArtifactEntry, signal?: AbortSignal): Promise<Readable> {
abortIfNeeded(signal);
if (entry.kind !== "file")
return Promise.reject(
new ArtifactReaderFailure("format", "ASAR entry is not a regular file"),
const state = this.#entries.get(entry.adapterKey);
if (state === undefined && entry.kind !== "file")
throw new ArtifactReaderFailure(
"format",
"ASAR entry is not a regular file",
);
if (state === undefined)
throw new ArtifactReaderFailure(
"integrity",
`ASAR entry was not produced by this reader: ${entry.path}`,
);
if (!sameArtifactEntry(entry, state.entry))
throw new ArtifactReaderFailure(
"integrity",
`ASAR entry metadata changed since inventory: ${state.entry.path}`,
);
const { metadata } = state;
const producedEntry = state.entry;
if (producedEntry.unpacked) {
const handle = await this.#openUnpackedEntry(producedEntry);
try {
const observed = await handle.stat();
abortIfNeeded(signal);
if (!observed.isFile())
throw new ArtifactReaderFailure(
"path",
`ASAR unpacked entry is not a regular file: ${producedEntry.path}`,
);
} catch (cause: unknown) {
await handle.close().catch(() => undefined);
throw asarFailure(this.path, `read ${producedEntry.path}`, cause);
}
const source = handle.createReadStream({
start: 0,
autoClose: true,
});
return readValidatedAsarEntry(
source,
undefined,
producedEntry.path,
this.path,
signal,
);
}
const archiveSize = this.#archiveSize;
const headerSize = this.#headerSize;
const offset =
metadata.offset === undefined
? undefined
: parseArchiveOffset(metadata.offset);
const start =
offset === undefined || headerSize === undefined
? undefined
: 8 + headerSize + offset;
if (
archiveSize === undefined ||
start === undefined ||
!Number.isSafeInteger(start) ||
metadata.size < 0 ||
!Number.isSafeInteger(metadata.size) ||
start > archiveSize ||
metadata.size > archiveSize - start
)
throw new ArtifactReaderFailure(
"format",
`ASAR entry range is outside its container: ${producedEntry.path}`,
);
let handle: FileHandle | undefined;
try {
return Promise.resolve(
Readable.from(extractFile(this.path, entry.adapterKey, false)),
const openedHandle = await open(
this.path,
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
);
handle = openedHandle;
const observed = await openedHandle.stat();
abortIfNeeded(signal);
if (!observed.isFile() || observed.size !== archiveSize)
throw new ArtifactReaderFailure(
"integrity",
`ASAR container changed before read: ${producedEntry.path}`,
);
if (metadata.size === 0) {
await closeAsarHandle(() => openedHandle.close(), signal);
handle = undefined;
return Readable.from([]);
}
const source = openedHandle.createReadStream({
start,
end: start + metadata.size - 1,
autoClose: true,
});
return readValidatedAsarEntry(
source,
metadata.size,
producedEntry.path,
this.path,
signal,
);
} catch (cause: unknown) {
if (entry.unpacked && isMissingFile(cause))
return Promise.reject(
new ArtifactReaderFailure(
"unavailable",
`ASAR unpacked entry bytes are unavailable: ${entry.path}`,
{ cause },
{
logicalPath: entry.path,
declaredSha256: entry.declaredSha256,
calculatedSha256: null,
unpacked: true,
},
),
);
return Promise.reject(
asarFailure(this.path, `read ${entry.path}`, cause),
);
await handle?.close().catch(() => undefined);
throw asarFailure(this.path, `read ${producedEntry.path}`, cause);
}
}
@@ -121,14 +218,154 @@ export class AsarArtifactReader implements ArtifactReader {
close(): Promise<void> {
uncache(this.path);
this.#resetArchiveState();
this.#entries.clear();
return Promise.resolve();
}
provenance(): readonly [] {
return [];
}
async #openUnpackedEntry(entry: ArtifactEntry): Promise<UnpackedFileHandle> {
const unpackedRoot = `${this.path}.unpacked`;
try {
const rootMetadata = await lstat(unpackedRoot);
if (!rootMetadata.isDirectory() || rootMetadata.isSymbolicLink())
throw new ArtifactReaderFailure(
"path",
"ASAR unpacked companion is not a regular directory",
);
const canonicalRoot = await realpath(unpackedRoot);
const candidate = join(unpackedRoot, entry.adapterKey);
const canonical = await realpath(candidate);
const relativePath = relative(canonicalRoot, canonical);
if (
relativePath === ".." ||
relativePath.startsWith(`..${sep}`) ||
isAbsolute(relativePath)
)
throw new ArtifactReaderFailure(
"path",
`ASAR unpacked entry escaped its companion directory: ${entry.path}`,
);
const pathMetadata = await lstat(candidate);
if (!pathMetadata.isFile() || pathMetadata.isSymbolicLink())
throw new ArtifactReaderFailure(
"path",
`ASAR unpacked entry is not a regular file: ${entry.path}`,
);
const handle = await this.openUnpackedFile(
canonical,
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
);
const openedMetadata = await handle
.stat()
.catch(async (cause: unknown) => {
await handle.close().catch(() => undefined);
throw cause;
});
if (
!openedMetadata.isFile() ||
openedMetadata.dev !== pathMetadata.dev ||
openedMetadata.ino !== pathMetadata.ino
) {
await handle.close();
throw new ArtifactReaderFailure(
"integrity",
`ASAR unpacked entry changed before read: ${entry.path}`,
{},
{
logicalPath: entry.path,
declaredSha256: entry.declaredSha256,
calculatedSha256: null,
unpacked: true,
},
);
}
return handle;
} catch (cause: unknown) {
if (cause instanceof ArtifactReaderFailure) throw cause;
if (entry.unpacked && isMissingFile(cause))
throw new ArtifactReaderFailure(
"unavailable",
`ASAR unpacked entry bytes are unavailable: ${entry.path}`,
{ cause },
{
logicalPath: entry.path,
declaredSha256: entry.declaredSha256,
calculatedSha256: null,
unpacked: true,
},
);
throw asarFailure(this.path, `read ${entry.path}`, cause);
}
}
#resetArchiveState(): void {
this.#archiveSize = undefined;
this.#headerSize = undefined;
}
}
type AsarFileMetadata = Extract<ReturnType<typeof statFile>, { size: number }>;
type AsarEntryState = {
readonly metadata: AsarFileMetadata;
readonly entry: ArtifactEntry;
};
type UnpackedFileHandle = {
stat(): Promise<Stats>;
close(): Promise<void>;
createReadStream: FileHandle["createReadStream"];
};
type OpenAsarUnpackedFile = (
path: string,
flags: number,
) => Promise<UnpackedFileHandle>;
const copyArtifactEntry = (entry: ArtifactEntry): ArtifactEntry => ({
...entry,
limitations: [...entry.limitations],
...(entry.sourceIdentity === undefined
? {}
: { sourceIdentity: { ...entry.sourceIdentity } }),
});
const sameArtifactEntry = (
entry: ArtifactEntry,
produced: ArtifactEntry,
): boolean =>
entry.path === produced.path &&
entry.kind === produced.kind &&
entry.declaredSize === produced.declaredSize &&
entry.compressedSize === produced.compressedSize &&
entry.executable === produced.executable &&
entry.encrypted === produced.encrypted &&
entry.byteOffset === produced.byteOffset &&
entry.declaredSha256 === produced.declaredSha256 &&
entry.unpacked === produced.unpacked &&
entry.adapterKey === produced.adapterKey &&
entry.limitations.length === produced.limitations.length &&
entry.limitations.every(
(limitation, index) => limitation === produced.limitations[index],
) &&
entry.sourceIdentity?.device === produced.sourceIdentity?.device &&
entry.sourceIdentity?.inode === produced.sourceIdentity?.inode;
const isAsarFileMetadata = (
metadata: ReturnType<typeof statFile>,
): metadata is AsarFileMetadata =>
"size" in metadata &&
typeof metadata.size === "number" &&
!("files" in metadata) &&
!("link" in metadata);
const parseArchiveOffset = (value: string): number | undefined => {
if (!/^(?:0|[1-9][0-9]*)$/u.test(value)) return undefined;
const offset = Number(value);
return Number.isSafeInteger(offset) ? offset : undefined;
};
const abortIfNeeded = (signal?: AbortSignal): void => {
if (signal?.aborted === true)
throw new ArtifactReaderFailure("cancelled", "ASAR operation cancelled");
+111
View File
@@ -0,0 +1,111 @@
import { Readable } from "node:stream";
import { ArtifactReaderFailure } from "./ArtifactReader.js";
/** Close an ASAR file handle before honoring cancellation of an empty member. */
export const closeAsarHandle = async (
close: () => Promise<void>,
signal?: AbortSignal,
): Promise<void> => {
await close();
abortIfNeeded(signal);
};
/** Validate and contextualize bytes read from one ASAR member source. */
export const readValidatedAsarEntry = (
source: Readable,
expectedBytes: number | undefined,
entryPath: string,
archivePath: string,
signal?: AbortSignal,
): Readable => {
const output = Readable.from(
(async function* () {
let observedBytes = 0;
const iterator = source[Symbol.asyncIterator]();
try {
while (true) {
abortIfNeeded(signal);
const next = await iterator.next();
if (next.done) break;
const chunk: unknown = next.value;
if (!Buffer.isBuffer(chunk) && !(chunk instanceof Uint8Array))
throw new ArtifactReaderFailure(
"format",
`ASAR entry did not return bytes: ${entryPath}`,
);
observedBytes += Buffer.isBuffer(chunk)
? chunk.length
: chunk.byteLength;
if (expectedBytes !== undefined && observedBytes > expectedBytes)
throw new ArtifactReaderFailure(
"integrity",
`ASAR entry exceeded its declared size: ${entryPath}`,
);
yield chunk;
}
if (expectedBytes !== undefined && observedBytes !== expectedBytes)
throw new ArtifactReaderFailure(
"integrity",
`ASAR entry size disagrees with its header: ${entryPath}`,
);
} catch (cause: unknown) {
throw asarFailure(archivePath, `read ${entryPath}`, cause);
} finally {
try {
await iterator.return?.();
} catch {
// Preserve the read failure; source destruction below owns cleanup.
}
if (!source.destroyed) source.destroy();
}
})(),
);
const closeSource = (): void => {
signal?.removeEventListener("abort", onAbort);
if (!source.destroyed) source.destroy();
};
const onAbort = (): void => {
closeSource();
output.destroy(
new ArtifactReaderFailure("cancelled", "ASAR operation cancelled"),
);
};
output.once("close", closeSource);
signal?.addEventListener("abort", onAbort, { once: true });
if (signal?.aborted === true) onAbort();
return output;
};
const abortIfNeeded = (signal?: AbortSignal): void => {
if (signal?.aborted === true)
throw new ArtifactReaderFailure("cancelled", "ASAR operation cancelled");
};
const asarFailure = (
path: string,
operation: string,
cause: unknown,
): ArtifactReaderFailure => {
if (cause instanceof ArtifactReaderFailure) return cause;
if (isFilesystemFailure(cause))
return new ArtifactReaderFailure(
"io",
`Could not ${operation} ASAR at ${path}: ${cause.message}`,
{ cause },
);
return new ArtifactReaderFailure(
"format",
`Malformed ASAR during ${operation}: ${path}`,
{ cause },
);
};
const isFilesystemFailure = (
cause: unknown,
): cause is NodeJS.ErrnoException & Error =>
cause instanceof Error &&
"errno" in cause &&
typeof cause.errno === "number" &&
"syscall" in cause &&
typeof cause.syscall === "string";
+61 -7
View File
@@ -13,34 +13,48 @@ import {
export class DirectoryArtifactReader implements ArtifactReader {
readonly format = "directory" as const;
readonly #rootPromise: Promise<string>;
readonly #rootPath: string;
constructor(root: string) {
this.#rootPath = root;
this.#rootPromise = realpath(root);
}
async *entries(signal?: AbortSignal): AsyncIterable<ArtifactEntry> {
const root = await this.#rootPromise;
const root = await this.#rootPromise.catch((cause: unknown) => {
throw directoryIoFailure("resolve artifact root", this.#rootPath, cause);
});
const pending = [root];
while (pending.length > 0) {
abortIfNeeded(signal);
const directory = pending.pop();
if (directory === undefined) break;
await assertContainedDirectory(root, directory);
await withDirectoryIoContext("inspect directory", directory, () =>
assertContainedDirectory(root, directory),
);
const children: string[] = [];
const handle = await opendir(directory);
const handle = await opendir(directory).catch((cause: unknown) => {
throw directoryIoFailure("open directory", directory, cause);
});
try {
for await (const child of handle) children.push(child.name);
} catch (cause: unknown) {
throw directoryIoFailure("read directory", directory, cause);
} finally {
// best-effort cleanup: directory-handle close must not mask traversal.
await handle.close().catch(() => undefined);
}
await assertContainedDirectory(root, directory);
await withDirectoryIoContext("inspect directory", directory, () =>
assertContainedDirectory(root, directory),
);
children.sort((left, right) => left.localeCompare(right, "en"));
const directories: string[] = [];
for (const name of children) {
abortIfNeeded(signal);
const absolute = join(directory, name);
const metadata = await lstat(absolute);
const metadata = await lstat(absolute).catch((cause: unknown) => {
throw directoryIoFailure("inspect entry", absolute, cause);
});
const path = relative(root, absolute).split(sep).join("/");
const kind = metadata.isSymbolicLink()
? "symlink"
@@ -86,9 +100,17 @@ export class DirectoryArtifactReader implements ArtifactReader {
const handle = await open(
entry.adapterKey,
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
);
).catch((cause: unknown) => {
throw directoryIoFailure("open entry", entry.adapterKey, cause);
});
try {
const observed = await handle.stat();
const observed = await handle.stat().catch((cause: unknown) => {
throw directoryIoFailure(
"inspect opened entry",
entry.adapterKey,
cause,
);
});
if (
!observed.isFile() ||
entry.sourceIdentity === undefined ||
@@ -117,6 +139,38 @@ export class DirectoryArtifactReader implements ArtifactReader {
}
}
const directoryIoFailure = (
operation: string,
path: string,
cause: unknown,
): ArtifactReaderFailure => {
if (cause instanceof ArtifactReaderFailure) return cause;
const code =
typeof cause === "object" &&
cause !== null &&
"code" in cause &&
typeof cause.code === "string"
? ` (${cause.code})`
: "";
return new ArtifactReaderFailure(
"io",
`Could not ${operation} at ${path}${code}`,
{ cause },
);
};
const withDirectoryIoContext = async <T>(
operation: string,
path: string,
action: () => Promise<T>,
): Promise<T> => {
try {
return await action();
} catch (cause: unknown) {
throw directoryIoFailure(operation, path, cause);
}
};
const abortIfNeeded = (signal?: AbortSignal): void => {
if (signal?.aborted === true)
throw new ArtifactReaderFailure(
+167 -10
View File
@@ -37,13 +37,18 @@ export interface NativeDmgHost {
arguments_: readonly string[],
signal?: AbortSignal,
options?: { readonly timeoutMs: number },
): Promise<{ readonly stdout: string; readonly exitCode: number }>;
): Promise<{
readonly stdout: string;
readonly stderr?: string;
readonly exitCode: number;
readonly cause?: unknown;
}>;
}
const systemHost: NativeDmgHost = {
async run(arguments_, signal, options) {
try {
const { stdout } = await execFileOutput(
const { stdout, stderr } = await execFileOutput(
"/usr/bin/hdiutil",
[...arguments_],
{
@@ -51,13 +56,23 @@ const systemHost: NativeDmgHost = {
...(signal === undefined ? {} : { signal }),
},
);
return { stdout, exitCode: 0 };
return { stdout, stderr, exitCode: 0 };
} catch (cause: unknown) {
if (cause instanceof Error && cause.name === "AbortError")
throw new ArtifactReaderFailure("cancelled", "DMG operation cancelled");
throw new ArtifactReaderFailure("format", "hdiutil rejected the DMG", {
cause,
});
const exitCode = processExitCode(cause);
if (exitCode !== undefined)
return {
stdout: processOutput(cause, "stdout"),
stderr: processOutput(cause, "stderr"),
exitCode,
cause,
};
throw new ArtifactReaderFailure(
commandFailureReason(cause, arguments_[0]),
`hdiutil ${arguments_[0] ?? "operation"} failed: ${describeCommandFailure(cause, arguments_)}`,
{ cause },
);
}
},
};
@@ -250,15 +265,157 @@ const runChecked = async (
signal?: AbortSignal,
options?: { readonly timeoutMs: number },
): Promise<{ readonly stdout: string; readonly exitCode: 0 }> => {
const result = await host.run(arguments_, signal, options);
if (result.exitCode !== 0)
let result: Awaited<ReturnType<NativeDmgHost["run"]>>;
try {
result = await host.run(arguments_, signal, options);
} catch (cause: unknown) {
if (cause instanceof ArtifactReaderFailure) throw cause;
if (cause instanceof Error && cause.name === "AbortError")
throw new ArtifactReaderFailure("cancelled", "DMG operation cancelled", {
cause,
});
throw new ArtifactReaderFailure(
"unavailable",
`hdiutil ${arguments_[0] ?? "operation"} failed with a non-zero exit code`,
commandFailureReason(cause, arguments_[0]),
`hdiutil ${arguments_[0] ?? "operation"} failed: ${describeCommandFailure(
cause,
arguments_,
)}`,
{ cause },
);
}
if (result.exitCode !== 0) {
const failure = {
command: "/usr/bin/hdiutil",
exitCode: result.exitCode,
...(result.cause === undefined
? {}
: { code: processExitCode(result.cause) }),
stdout: result.stdout,
...(result.stderr === undefined ? {} : { stderr: result.stderr }),
};
const reason = commandFailureReason(failure, arguments_[0]);
const details = describeCommandFailure(failure, arguments_);
const unknownVerifyFailure =
arguments_[0] === "verify" && reason === "unavailable";
throw new ArtifactReaderFailure(
reason,
unknownVerifyFailure
? `hdiutil verify failed; captured diagnostics do not establish the failure cause: ${details}`
: `hdiutil ${arguments_[0] ?? "operation"} failed: ${details}`,
result.cause === undefined ? undefined : { cause: result.cause },
);
}
return { stdout: result.stdout, exitCode: 0 };
};
const describeCommandFailure = (
cause: unknown,
arguments_: readonly string[],
): string => {
const fields: Record<string, string | number | readonly string[]> = {
command: "/usr/bin/hdiutil",
arguments: [...arguments_],
};
if (typeof cause === "object" && cause !== null) {
for (const key of [
"code",
"exitCode",
"errno",
"syscall",
"signal",
"stdout",
"stderr",
] as const) {
const value = Reflect.get(cause, key);
if (typeof value === "string" || typeof value === "number")
fields[key] = value;
}
const code = Reflect.get(cause, "code");
if (typeof code === "number") fields.exitCode = code;
}
if (cause instanceof ArtifactReaderFailure) fields.message = cause.message;
else if (cause instanceof Error) fields.message = cause.message;
return JSON.stringify(fields);
};
const processExitCode = (cause: unknown): number | undefined => {
if (typeof cause !== "object" || cause === null) return undefined;
const exitCode = Reflect.get(cause, "exitCode");
if (typeof exitCode === "number") return exitCode;
const code = Reflect.get(cause, "code");
return typeof code === "number" ? code : undefined;
};
const processOutput = (cause: unknown, field: "stdout" | "stderr"): string => {
if (typeof cause !== "object" || cause === null) return "";
const value = Reflect.get(cause, field);
return typeof value === "string" ? value : "";
};
const commandFailureReason = (
cause: unknown,
operation: string | undefined,
): ArtifactReaderFailure["reason"] => {
if (typeof cause !== "object" || cause === null) return "unavailable";
const code = Reflect.get(cause, "code");
const exitCode = Reflect.get(cause, "exitCode");
if (typeof code === "number" || typeof exitCode === "number")
return operation === "verify" ? verifyFailureReason(cause) : "unavailable";
if (code === "ENOENT") {
const syscall = Reflect.get(cause, "syscall");
return typeof syscall === "string" && syscall.startsWith("spawn")
? "unavailable"
: "io";
}
if (
code === "EACCES" ||
code === "EPERM" ||
code === "EIO" ||
code === "ENOTDIR" ||
code === "EISDIR" ||
code === "ENODEV" ||
code === "EROFS" ||
code === "EMFILE" ||
code === "ENFILE"
)
return "io";
return "unavailable";
};
const verifyFailureReason = (
cause: unknown,
): ArtifactReaderFailure["reason"] => {
const diagnostic = verifyFailureDiagnostic(
processOutput(cause, "stdout"),
processOutput(cause, "stderr"),
);
switch (diagnostic) {
case "image not recognized":
return "format";
case "invalid checksum":
case "image data corrupted":
return "integrity";
case "No such file or directory":
case "Permission denied":
case "Input/output error":
return "io";
default:
return "unavailable";
}
};
const verifyFailureDiagnostic = (
stdout: string,
stderr: string,
): string | undefined => {
const prefix = "hdiutil: verify failed - ";
return `${stdout}\n${stderr}`
.split(/\r?\n/u)
.map((line) => line.trim())
.find((line) => line.startsWith(prefix))
?.slice(prefix.length);
};
const command = (
arguments_: readonly string[],
effects: ArtifactCommand["effects"],
@@ -1,14 +1,14 @@
import { createHash } from "node:crypto";
import { lstat } from "node:fs/promises";
import { DirectoryArtifactReader } from "../artifacts/DirectoryArtifactReader.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js";
import { ArtifactReaderFailure } from "../ArtifactReader.js";
import { analyzeInterfaceBuilderBundle } from "./InterfaceBuilderAnalysis.js";
import {
appleAssetCatalogInputSchema,
parseAppleAssetCatalogRecords,
projectAppleAssetCatalogPage,
} from "../domain/appleAssetCatalog.js";
import { execFileOutput } from "../process/ExecFileOutput.js";
} from "../../domain/apple/appleAssetCatalog.js";
import { execFileOutput } from "../../process/ExecFileOutput.js";
const ASSETUTIL = "/usr/bin/assetutil";
const MAX_CATALOG_BYTES = 64 * 1024 * 1024;
@@ -105,7 +105,7 @@ export const analyzeAppleAssetCatalogs = async (input: {
const inspectAssetCatalogEntry = async (input: {
readonly reader: DirectoryArtifactReader;
readonly entry: import("../artifacts/ArtifactReader.js").ArtifactEntry;
readonly entry: import("../ArtifactReader.js").ArtifactEntry;
readonly runAssetUtil: (
path: string,
signal?: AbortSignal,
@@ -237,7 +237,7 @@ const runAssetUtil = async (
const digestBounded = async (
reader: DirectoryArtifactReader,
entry: import("../artifacts/ArtifactReader.js").ArtifactEntry,
entry: import("../ArtifactReader.js").ArtifactEntry,
signal?: AbortSignal,
): Promise<string> => {
if (entry.declaredSize === null || entry.declaredSize > MAX_CATALOG_BYTES)
@@ -1,28 +1,25 @@
import { createHash } from "node:crypto";
import { parseBinary } from "plist";
import { DirectoryArtifactReader } from "../artifacts/DirectoryArtifactReader.js";
import { projectPlistValue } from "../domain/plistValue.js";
import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js";
import { projectPlistValue } from "../../domain/apple/plistValue.js";
import {
omittedPrototypeKeysLimitation,
parseXmlPropertyList,
} from "../domain/propertyListKeys.js";
import {
decodeNibArchive,
type NibArchiveDocument,
} from "../artifacts/NibArchive.js";
import type { ArtifactEntry } from "../artifacts/ArtifactReader.js";
} from "../../domain/propertyListKeys.js";
import { decodeNibArchive, type NibArchiveDocument } from "./NibArchive.js";
import type { ArtifactEntry } from "../ArtifactReader.js";
import {
buildInterfaceBuilderAnalysis,
interfaceBuilderLimitsSchema,
type InterfaceBuilderDocumentInput,
} from "../domain/interfaceBuilderGraph.js";
} from "../../domain/apple/interfaceBuilderGraph.js";
import {
mergeNibHierarchies,
projectNibViewHierarchy,
type NibHierarchyNode,
} from "./NibViewHierarchy.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import { jsonValueSchema, type JsonValue } from "../../domain/jsonValue.js";
const MAX_DOCUMENT_BYTES = 64 * 1024 * 1024;

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