Compare commits
119
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8288ef1c8 | ||
|
|
9fe37142ee | ||
|
|
16777fe7b0 | ||
|
|
f5fbbc3fdf | ||
|
|
f7a5b66df2 | ||
|
|
a65755728f | ||
|
|
7acf4e741a | ||
|
|
c7530f9a81 | ||
|
|
6d74992efe | ||
|
|
3e24a72465 | ||
|
|
dda8e6a728 | ||
|
|
2366bda37b | ||
|
|
e0d76ae80d | ||
|
|
f14b6a8430 | ||
|
|
118d43e787 | ||
|
|
c92a344d47 | ||
|
|
ac3bae3cb9 | ||
|
|
348c65c7ec | ||
|
|
9e39795895 | ||
|
|
2e81e867b2 | ||
|
|
810fe53e79 | ||
|
|
28713f9b4d | ||
|
|
2ccc86cd13 | ||
|
|
dcde958450 | ||
|
|
e6ba9c3af3 | ||
|
|
37d2ebac4c | ||
|
|
45fcd28b40 | ||
|
|
9226ade511 | ||
|
|
aa8082ea6c | ||
|
|
b3f013fa18 | ||
|
|
2ed89e6529 | ||
|
|
88c940b165 | ||
|
|
d93ebec9df | ||
|
|
fd552a8c4b | ||
|
|
be121d87eb | ||
|
|
4f25ee33dc | ||
|
|
b9292516f4 | ||
|
|
f826aa893f | ||
|
|
55249704c5 | ||
|
|
b956bd589f | ||
|
|
62634e4c01 | ||
|
|
28fd49f0f6 | ||
|
|
4f5110306d | ||
|
|
9a15525298 | ||
|
|
27c272bd9a | ||
|
|
dd3e7aef0d | ||
|
|
b72c079a9a | ||
|
|
1d769cd261 | ||
|
|
071cfd1bb6 | ||
|
|
f0a68dce60 | ||
|
|
2db668f6ca | ||
|
|
7a9f587c3a | ||
|
|
900d7c8606 | ||
|
|
62b37111e7 | ||
|
|
5574287101 | ||
|
|
5b5b76a583 | ||
|
|
4d2faef37b | ||
|
|
96a9f32dc1 | ||
|
|
f6a58f1062 | ||
|
|
d5f28fb29c | ||
|
|
15ab5dbcaf | ||
|
|
4e3953cc45 | ||
|
|
8b243877c2 | ||
|
|
418a5562f6 | ||
|
|
7714e763d9 | ||
|
|
df7151ba30 | ||
|
|
53bfa7e3f7 | ||
|
|
e52fe9719f | ||
|
|
d421d3f286 | ||
|
|
aeebba391e | ||
|
|
c78fe6990b | ||
|
|
eab34a9d8c | ||
|
|
73370e1c06 | ||
|
|
c60b737f7d | ||
|
|
b08228d19f | ||
|
|
767c6c9a95 | ||
|
|
f430c416c7 | ||
|
|
4801a24089 | ||
|
|
5afd8a850b | ||
|
|
9f48a76e11 | ||
|
|
2a4667bb79 | ||
|
|
0804722cdf | ||
|
|
898b0f62d7 | ||
|
|
6729ee7636 | ||
|
|
e5ba6934e4 | ||
|
|
faaee693d5 | ||
|
|
05f43ed388 | ||
|
|
76b70203d9 | ||
|
|
b808e2d6bb | ||
|
|
4037c6545d | ||
|
|
d52af3b321 | ||
|
|
d98dcfa8c2 | ||
|
|
dd114d3980 | ||
|
|
ab985c6a10 | ||
|
|
e32028d1b5 | ||
|
|
01be082e9e | ||
|
|
a08be51e9f | ||
|
|
507e8b3c7b | ||
|
|
e3e7664665 | ||
|
|
c371d6e1e9 | ||
|
|
14f9b3d931 | ||
|
|
37fe105ba1 | ||
|
|
9075ce71ad | ||
|
|
00f707b250 | ||
|
|
ae849a7281 | ||
|
|
5738a0cf8c | ||
|
|
895079dd09 | ||
|
|
eb68761eb0 | ||
|
|
602d0021d4 | ||
|
|
0674facd46 | ||
|
|
8e8921d40c | ||
|
|
63e0822cc7 | ||
|
|
cfd2b0c75d | ||
|
|
dd90965058 | ||
|
|
77cf757103 | ||
|
|
021ba690b0 | ||
|
|
01779a6f61 | ||
|
|
35c82b93eb | ||
|
|
6c88094da4 |
@@ -239,20 +239,46 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
package-e2e:
|
||||
name: Package/install integration (${{ matrix.os }})
|
||||
name: Native package baseline (${{ matrix.platform }} ${{ matrix.architecture }})
|
||||
needs: [changes, windows-native-build]
|
||||
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 2
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-14]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
platform: linux
|
||||
architecture: x64
|
||||
- os: ubuntu-24.04-arm
|
||||
platform: linux
|
||||
architecture: arm64
|
||||
- os: macos-15
|
||||
platform: darwin
|
||||
architecture: arm64
|
||||
- os: macos-15-intel
|
||||
platform: darwin
|
||||
architecture: x64
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=1024"
|
||||
UV_THREADPOOL_SIZE: "2"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Verify native runtime identity
|
||||
env:
|
||||
EXPECTED_CI_PLATFORM: ${{ matrix.platform }}
|
||||
EXPECTED_CI_ARCHITECTURE: ${{ matrix.architecture }}
|
||||
run: >-
|
||||
node -e "const assert = require('node:assert/strict');
|
||||
assert.equal(process.platform, process.env.EXPECTED_CI_PLATFORM);
|
||||
assert.equal(process.arch, process.env.EXPECTED_CI_ARCHITECTURE);
|
||||
console.log(JSON.stringify({ platform: process.platform, architecture: process.arch, node: process.version }));"
|
||||
- run: npm ci
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -260,12 +286,15 @@ jobs:
|
||||
path: native/windows/build/
|
||||
- run: npm run verify:windows-native:artifact
|
||||
- run: npm run verify:package
|
||||
- name: Verify real Node Inspector discovery and observation
|
||||
if: matrix.platform == 'darwin'
|
||||
run: npm run verify:inspector
|
||||
|
||||
apple-artifacts:
|
||||
name: Real Apple artifact CLI/MCP E2E
|
||||
needs: changes
|
||||
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -276,6 +305,7 @@ jobs:
|
||||
- run: npm ci
|
||||
- run: npm run verify:keyed-archive
|
||||
- run: npm run verify:asset-catalog
|
||||
- run: npm run verify:macos-bundle
|
||||
- name: Native compiled NIB hierarchy regressions
|
||||
run: npm run test:focused -- tests/boundary/filesystem/nibNativeHierarchy.test.ts tests/boundary/filesystem/interfaceBuilderBundle.test.ts
|
||||
- name: Real binary metadata and Interface Builder integration
|
||||
@@ -318,16 +348,38 @@ jobs:
|
||||
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 2
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
platform: linux
|
||||
architecture: x64
|
||||
- os: ubuntu-24.04-arm
|
||||
platform: linux
|
||||
architecture: arm64
|
||||
- os: windows-latest
|
||||
platform: win32
|
||||
architecture: x64
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=1024"
|
||||
UV_THREADPOOL_SIZE: "2"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- name: Verify native runtime identity
|
||||
env:
|
||||
EXPECTED_CI_PLATFORM: ${{ matrix.platform }}
|
||||
EXPECTED_CI_ARCHITECTURE: ${{ matrix.architecture }}
|
||||
run: >-
|
||||
node -e "const assert = require('node:assert/strict');
|
||||
assert.equal(process.platform, process.env.EXPECTED_CI_PLATFORM);
|
||||
assert.equal(process.arch, process.env.EXPECTED_CI_ARCHITECTURE);
|
||||
console.log(JSON.stringify({ platform: process.platform, architecture: process.arch, node: process.version }));"
|
||||
- run: npm ci
|
||||
- name: Verify real Node Inspector discovery and observation
|
||||
run: npm run verify:inspector
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Documentation build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: documentation-build-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build documentation
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run docs:build
|
||||
@@ -7,18 +7,31 @@ on:
|
||||
- "scripts/verify-real-browser.mjs"
|
||||
- "scripts/verify-browser-network.mjs"
|
||||
- "scripts/verify-browser-scripts.mjs"
|
||||
- "scripts/verify-browser-modules.mjs"
|
||||
- "scripts/verify-browser-dom-destinations.mjs"
|
||||
- "scripts/lib/browser-*.mjs"
|
||||
- "scripts/fixtures/browser-network-site.mjs"
|
||||
- "scripts/fixtures/browser-script-site.mjs"
|
||||
- "scripts/fixtures/browser-module-site.mjs"
|
||||
- "scripts/fixtures/browser-verifier-site.mjs"
|
||||
- "scripts/fixtures/page-cdp-proxy.mjs"
|
||||
- "src/browser/**"
|
||||
- "src/domain/browser*.ts"
|
||||
- "src/domain/webScript*.ts"
|
||||
- "src/domain/webModule*.ts"
|
||||
- "src/application/WebModule*.ts"
|
||||
- "src/composition/webModules.ts"
|
||||
- "src/artifacts/readStableArtifact.ts"
|
||||
- "src/artifacts/ArtifactPaths.ts"
|
||||
- "src/application/WebScriptExportService.ts"
|
||||
- "src/contracts/webScriptToolContracts.ts"
|
||||
- "src/server/registerWebScriptTool.ts"
|
||||
- "src/server/registerWebModuleTool.ts"
|
||||
- "src/server/createServer.ts"
|
||||
- "src/cli/webModuleCommands.ts"
|
||||
- "src/cli.ts"
|
||||
- "src/cliCommandNames.ts"
|
||||
- "src/contracts/toolEffects.ts"
|
||||
- "src/cli/webScriptCommands.ts"
|
||||
- "package.json"
|
||||
- "package-lock.json"
|
||||
|
||||
@@ -5,10 +5,10 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/real-javascript-recovery.yml"
|
||||
- "src/javascript/recovery/**"
|
||||
- "src/domain/javascript*.ts"
|
||||
- "src/application/JavaScriptRecovery*.ts"
|
||||
- "src/domain/javascript/**"
|
||||
- "src/application/javascript/JavaScriptRecovery*.ts"
|
||||
- "src/composition/javascriptRecovery.ts"
|
||||
- "src/contracts/javascriptRecoveryToolContracts.ts"
|
||||
- "src/contracts/javascript/javascriptRecoveryToolContracts.ts"
|
||||
- "src/cli/javascriptRecoveryCommands.ts"
|
||||
- "src/cli/javascriptApplicationAnalysis.ts"
|
||||
- "src/server/registerJavaScriptRecoveryTool.ts"
|
||||
@@ -22,8 +22,7 @@ on:
|
||||
- "scripts/rea.mjs"
|
||||
- "src/process/**"
|
||||
- "src/artifacts/SafeOutputTree.ts"
|
||||
- "scripts/verify-javascript-recovery.mjs"
|
||||
- "scripts/lib/javascript-recovery-*.mjs"
|
||||
- "scripts/verify/javascript/recovery.mjs"
|
||||
- "scripts/fixtures/javascript-recovery/**"
|
||||
- "third_party/wakaru"
|
||||
- "package.json"
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
name: Real website source-map verification
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/real-web-source-map.yml"
|
||||
- "scripts/verify-browser-source-maps.mjs"
|
||||
- "scripts/lib/browser-source-map-e2e.mjs"
|
||||
- "scripts/fixtures/browser-source-map-site.mjs"
|
||||
- "src/javascript/sourceMaps/**"
|
||||
- "src/browser/assets/**"
|
||||
- "src/domain/webSource*.ts"
|
||||
- "src/domain/webScript*.ts"
|
||||
- "src/application/WebSourceLocation*.ts"
|
||||
- "src/application/ValidateSelectedWebScript.ts"
|
||||
- "src/composition/webSourceLocations.ts"
|
||||
- "src/server/registerWebSourceLocationTool.ts"
|
||||
- "src/server/createServer.ts"
|
||||
- "src/contracts/webScriptToolContracts.ts"
|
||||
- "src/process/**"
|
||||
- "src/cli/webSourceLocationCommands.ts"
|
||||
- "src/cli.ts"
|
||||
- "src/artifacts/readStableArtifact.ts"
|
||||
- "package.json"
|
||||
- "package-lock.json"
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
contents: read
|
||||
concurrency:
|
||||
group: real-web-source-map-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
env:
|
||||
HUSKY: "0"
|
||||
jobs:
|
||||
real-web-source-map:
|
||||
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
|
||||
- name: Install the pinned compiler into an isolated fixture directory
|
||||
run: npm install --prefix "$RUNNER_TEMP/rea-web-source-map-compiler" --no-audit --no-fund --package-lock=false esbuild@0.25.10
|
||||
- run: npm run verify:browser:source-maps
|
||||
env:
|
||||
REA_BROWSER_EXECUTABLE: /usr/bin/google-chrome
|
||||
REA_BROWSER_NO_SANDBOX: "true"
|
||||
REA_WEB_SOURCE_MAP_COMPILER: ${{ runner.temp }}/rea-web-source-map-compiler/node_modules/esbuild/lib/main.js
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Website checks
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "website/**"
|
||||
- "scripts/verify-website.py"
|
||||
- ".github/workflows/**"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: website-check-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Website checks
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Check website links and publishing configuration
|
||||
run: python3 scripts/verify-website.py
|
||||
@@ -0,0 +1,46 @@
|
||||
name: Publish REA website
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: website-pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
pages: read
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Check website links and publishing configuration
|
||||
run: python3 scripts/verify-website.py
|
||||
- name: Configure Pages
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Upload the website
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: website/public
|
||||
|
||||
deploy:
|
||||
needs: prepare
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Publish to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
@@ -37,6 +37,7 @@ coverage/
|
||||
test-results.xml
|
||||
.turbo/
|
||||
.cache/
|
||||
docs/.vitepress/cache/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
+2
-1
@@ -28,7 +28,8 @@
|
||||
{
|
||||
"files": [
|
||||
"scripts/electron-active-hook*.cjs",
|
||||
"tests/conformance/readiness/electron/*.js"
|
||||
"tests/conformance/readiness/electron/*.js",
|
||||
"website/public/examples/notes-electron/*.js"
|
||||
],
|
||||
"rules": {
|
||||
"no-var-requires": "off"
|
||||
|
||||
@@ -20,10 +20,10 @@ REA is a local-only tool. Preserve caller-selected inputs, captured output, URLs
|
||||
REA is a layered ESM TypeScript application. Dependencies flow inward from pure domain logic through contracts, providers, application workflows, and CLI/MCP adapters. See [docs/architecture.mermaid](docs/architecture.mermaid) for the component map.
|
||||
|
||||
- `src/domain/` owns pure provider-neutral semantics; `src/contracts/` owns caller-visible schemas and the canonical tool inventory.
|
||||
- `src/hopper/`, `src/ghidra/`, `src/ida/`, `src/browser/`, `src/native/`, `src/artifacts/`, and `src/dotnet/` own provider-specific boundaries. Keep provider protocols out of domain and application code.
|
||||
- `src/hopper/`, `src/ghidra/`, `src/ida/`, `src/browser/`, `src/inspector/`, `src/native/`, `src/artifacts/`, and `src/dotnet/` own provider-specific boundaries. Keep provider protocols out of domain and application code.
|
||||
- `src/application/` composes shared CLI/MCP workflows; `src/server/` translates MCP requests; `src/cli.ts` and `src/main.ts` are the CLI and MCP entry points.
|
||||
- `src/process/` owns shared process lifecycle primitives, not provider wire protocols. `bridge/` contains provider-side adapters.
|
||||
- `tests/` contains unit, composition, boundary, acceptance, and conformance tests. `scripts/verify-*` contains real-toolchain checks.
|
||||
- `tests/` contains unit, composition, boundary, acceptance, and conformance tests. `scripts/verify-*` and capability directories under `scripts/verify/` contain real-toolchain checks.
|
||||
- `docs/product-catalog.json` is generated. Update its source contracts and regenerate it; do not edit it directly.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
+17
-1
@@ -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
|
||||
@@ -26,6 +26,22 @@ worktrees. After a package, lockfile, or managed-skill version change, run
|
||||
|
||||
Keep dependencies flowing inward through the existing domain, contracts, provider, application, server, and adapter layers. Parse unknown values at process and protocol boundaries, model expected failures with `Result`, and preserve the canonical tool inventory defined by `TOOL_CONTRACTS` unless a deliberate contract change updates every verifier, generated catalog artifact, and snapshot. Keep tool discovery complete and report capability- and session-scoped availability through `binary_session`.
|
||||
|
||||
## Documentation website
|
||||
|
||||
The VitePress site uses the Markdown files in `docs/` and deploys to
|
||||
<https://morluto.github.io/rea/>. Run `npm run docs:dev` for live editing,
|
||||
`npm run docs:build` to check the production build and links, and
|
||||
`npm run docs:preview` to preview that build at `/rea/`.
|
||||
|
||||
Site navigation lives in `docs/.vitepress/config.ts`. Keep links to guides
|
||||
relative so they work on GitHub and the website; link to repository files
|
||||
outside `docs/` using their full GitHub URLs. Generated reference documents
|
||||
still use `npm run docs:generate`; `docs:build` only builds the website.
|
||||
|
||||
The documentation workflow builds pull requests and deploys changes on `main`.
|
||||
The repository's **Settings → Pages → Build and deployment → Source** must be
|
||||
set to **GitHub Actions** before the first deployment.
|
||||
|
||||
## Development feedback and PR verification
|
||||
|
||||
For an ordinary edit, run the relevant regression and cached static checks:
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&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/)**
|
||||
|
||||
[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)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
@@ -155,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
|
||||
|
||||
@@ -163,7 +165,7 @@ Deep native binary analysis requires [Hopper](https://www.hopperapp.com/), [Ghid
|
||||
|
||||
Firmware region inspection and explicit extraction use caller-supplied Binwalk and Unblob on Linux. See [Firmware analysis](docs/firmware-analysis.md) for setup, provenance, resource limits and native handoff.
|
||||
|
||||
Static APK analysis uses a separately supplied headless JADX JAR and Java, with no emulator or APK execution. See [Android analysis](docs/android-analysis.md) for setup, CLI/MCP operations, coverage and public test fixtures. Authenticated IPA inventory Evidence can be projected with [Apple application analysis](docs/apple-application-analysis.md).
|
||||
Static APK analysis uses a separately supplied headless JADX JAR and a full JDK, with no emulator or APK execution. See [Android analysis](docs/android-analysis.md) for setup, CLI/MCP operations, coverage and public test fixtures. Authenticated IPA and macOS `.app`, ZIP, or DMG inventory Evidence can be projected into bundle anatomy, such as XPC services, app extensions, login items, privileged helpers, and launchd plists, with [Apple application analysis](docs/apple-application-analysis.md).
|
||||
|
||||
Repository main and npm 4.1.0 include experimental Windows x64 Ghidra support for native x86-64 PE applications on local NTFS, with bundled Job Object, private-DACL, and path-admission controls. Check the [release boundary](docs/installation.md#released-package-and-main) before expecting this from an older npm package. See [Windows Ghidra P0](docs/windows-ghidra-p0.md) for prerequisites and verified scope.
|
||||
|
||||
@@ -181,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
|
||||
@@ -191,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
|
||||
|
||||
@@ -369,6 +371,21 @@ REA gives the agent a clear path from that request to working code:
|
||||
|
||||
REA handles the app analysis in steps 1 through 5. The agent performs step 6 with its normal file-editing and test tools, using what it learned about the app.
|
||||
|
||||
## Showcase
|
||||
|
||||
### DX-Ball: game reconstruction (in progress)
|
||||
|
||||
[DX-Ball](https://github.com/N0zoM1z0/dx-ball) follows the journey from a classic
|
||||
Windows game's executable to maintainable C. Using REA's Ghidra provider, the
|
||||
project traces functions, game state and dependencies, then checks reconstructed
|
||||
behavior with original-x86 differential tests and pinned-compiler replay.
|
||||
|
||||
A [sound-pan investigation](https://github.com/N0zoM1z0/dx-ball/blob/main/docs/GAMEPLAY_OWNER.md)
|
||||
turns incomplete pseudocode into a C implementation that passes 3,205 original-x86
|
||||
cases and reproduces all 63 compiled function bytes. Follow its
|
||||
[REA workflow](https://github.com/N0zoM1z0/dx-ball/blob/main/docs/REA.md)
|
||||
from binary evidence to reconstruction.
|
||||
|
||||
## What agents can do
|
||||
|
||||
- Investigate a feature you like and build a version tailored to your own product.
|
||||
@@ -396,7 +413,7 @@ See [native investigation](docs/native-investigation.md) for keyed archives, ins
|
||||
| Browser observation | 9 | page structure, network metadata, scripts, source maps, WebMCP discovery, screenshots, and capture comparisons |
|
||||
| Electron analysis | 5 | renderer observation, static app mapping, and static/runtime reconciliation |
|
||||
| JavaScript runtime | 2 | Node/Electron Inspector target discovery, script locations, and execution-context events |
|
||||
| Application workflows | 11 | captured website script export; Android/Apple inventory projections; cross-layer feature traces, build comparisons, historical source mapping, static return-shape comparison, and reconstruction checks |
|
||||
| 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 |
|
||||
| Workspace and observation | 21 | sessions, evidence bundles, navigation context, process/artifact/function comparisons, and open-question tracking |
|
||||
|
||||
The public interface describes what the agent is trying to learn. Providers decide how to answer. macOS utilities handle common semantic inspection without launching Hopper; Hopper handles deeper native analysis; the process harness records direct behavioral captures.
|
||||
@@ -405,7 +422,7 @@ The public interface describes what the agent is trying to learn. Providers deci
|
||||
|
||||
REA supports native application, JavaScript, Electron, .NET, and browser investigation on macOS and Linux. Individual tools have platform and runtime prerequisites. `rea capabilities` and `rea providers` describe the binary-session providers and auxiliary operations; they are not an inventory of every browser, Android, or application workflow. Use the connected MCP tool list and `binary_session` tool availability for the full MCP surface, and the relevant guide for each tool's prerequisites. Repository main can be ahead of [the npm release](docs/installation.md#released-package-and-main).
|
||||
|
||||
Static Android APK inspection is verified on Linux with headless JADX; see [Android analysis](docs/android-analysis.md) for its separate prerequisites and coverage.
|
||||
Static Android APK inspection supports Linux and macOS; the current metadata bridge is verified on macOS arm64 with headless JADX; see [Android analysis](docs/android-analysis.md) for its separate prerequisites and coverage.
|
||||
|
||||
- **Native binaries:** Open Mach-O, ELF, PE, and Mac `.app` targets through a selected deep provider. Hopper and Ghidra cover broad inventory and function analysis; the IDA adapter supplies its documented read-only function/string operations. Hopper also accepts `.hop` databases and supports annotations.
|
||||
- **Packages and resources:** Inspect directories, ZIP, APK, IPA, ASAR, plists, compiled Interface Builder files, and Apple asset catalogs. Artifact requests name the input and requested extraction or traversal directly; macOS DMG traversal also requires the host's native mounting support.
|
||||
@@ -417,6 +434,15 @@ Static Android APK inspection is verified on Linux with headless JADX; see [Andr
|
||||
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).
|
||||
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
|
||||
`REA_BROWSER_EXECUTABLE`; see [module relationships](docs/website-module-trace.md).
|
||||
|
||||
- **Source locations:** Trace one retained website script point through a selected
|
||||
local source map with `rea trace-web-source-location`. See
|
||||
[captured website source locations](docs/web-source-location.md) for byte
|
||||
identities, embedded original content and coverage limits.
|
||||
Recover readable modules from selected local bundles with
|
||||
`recover_javascript_sources` / `rea recover-javascript-sources`, then pass
|
||||
the returned `analysis_input` to static analysis. This optional Linux x64
|
||||
@@ -791,3 +817,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and contribution ch
|
||||
<img alt="REA GitHub star history" src="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Disclaimer
|
||||
|
||||
REA provides tools for lawful reverse-engineering research, analysis, and reconstruction. You are responsible for obtaining any required authorization and complying with applicable laws. The project does not endorse illegal or unauthorized use.
|
||||
|
||||
+6
-4
@@ -17,6 +17,8 @@
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&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>
|
||||
|
||||
**[الموقع (بالإنجليزية)](https://morluto.github.io/rea/) · [أدلة الاستخدام](https://morluto.github.io/rea/guides/) · [دراسة حالة DX-Ball](https://morluto.github.io/rea/showcase/dx-ball/)**
|
||||
|
||||
[البدء السريع](#البدء-السريع) · [حالة الدعم](#حالة-الدعم) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [منصة أدوات التحقيق](#منصة-أدوات-التحقيق) · [خطة العمل](#خطة-العمل) · [كيف يعمل؟](#كيف-يعمل)
|
||||
|
||||
<table aria-label="REA community">
|
||||
@@ -136,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 برنامج منفصل، وللوضع التجريبي قيود يحددها المورّد، لكن الترخيص المدفوع ليس إلزاميًا.
|
||||
@@ -151,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).
|
||||
|
||||
### التحديث والإزالة
|
||||
|
||||
@@ -223,7 +225,7 @@ rea setup
|
||||
| مراقبة المتصفح | 9 | بنية الصفحات وبيانات الشبكة والبرامج النصية وخرائط المصدر واكتشاف WebMCP والصور ومقارنة الالتقاط |
|
||||
| تحليل Electron | 5 | مراقبة العارضين وخريطة التطبيق الثابتة والمواءمة بين نتائج الثبات والتشغيل |
|
||||
| وقت تشغيل JavaScript | 2 | اكتشاف أهداف Node/Electron Inspector ومواضع البرامج وأحداث سياقات التنفيذ |
|
||||
| سير عمل التطبيقات | 11 | تصدير النصوص البرمجية الملتقطة، تتبع الميزات عبر الطبقات ومقارنة الإصدارات ومواءمة المصدر التاريخي ومقارنة أشكال القيم المعادة والتحقق من إعادة الإنشاء |
|
||||
| سير عمل التطبيقات | 13 | تصدير النصوص البرمجية الملتقطة، تتبع الميزات عبر الطبقات ومقارنة الإصدارات ومواءمة المصدر التاريخي ومقارنة أشكال القيم المعادة والتحقق من إعادة الإنشاء |
|
||||
| مساحة العمل والملاحظة | 21 | الجلسات وحزم الأدلة وسياق التنقل ومقارنة العمليات والمخرجات والدوال وتسجيل الأسئلة غير المحسومة |
|
||||
|
||||
## خطة العمل
|
||||
@@ -311,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>
|
||||
|
||||
|
||||
+5
-3
@@ -17,6 +17,8 @@
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&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>
|
||||
|
||||
**[ウェブサイト(英語)](https://morluto.github.io/rea/) · [ガイド](https://morluto.github.io/rea/guides/) · [DX-Ball の事例](https://morluto.github.io/rea/showcase/dx-ball/)**
|
||||
|
||||
[クイックスタート](#クイックスタート) · [現在の対応状況](#現在の対応状況) · [バイナリから動作へ](#バイナリから動作へ) · [調査ツールカタログ](#調査ツールカタログ) · [ロードマップ](#ロードマップ) · [仕組み](#仕組み)
|
||||
|
||||
<table aria-label="REA community">
|
||||
@@ -136,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 は別製品です。デモにはベンダー所定の制限がありますが、有料ライセンスは必須ではありません。
|
||||
@@ -151,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)を参照してください。
|
||||
|
||||
### 更新とアンインストール
|
||||
|
||||
@@ -212,7 +214,7 @@ REA は手順 1〜5 のバイナリ解析を処理し、手順 6 はエージェ
|
||||
| ブラウザー観察 | 9 | ページ構成、ネットワークメタデータ、スクリプト、ソースマップ、WebMCP 検出、スクリーンショット、キャプチャ比較 |
|
||||
| Electron 解析 | 5 | レンダラー観察、静的アプリマッピング、静的/実行時結果の対応付け |
|
||||
| JavaScript 実行時 | 2 | Node/Electron Inspector 対象の検出、スクリプト位置、実行コンテキストイベント |
|
||||
| アプリワークフロー | 11 | 取得した Web スクリプトのエクスポート、層をまたぐ機能トレース、ビルド比較、履歴ソース対応、静的戻り値形状の比較、再実装検証 |
|
||||
| アプリワークフロー | 13 | 取得した Web スクリプトのエクスポート、層をまたぐ機能トレース、ビルド比較、履歴ソース対応、静的戻り値形状の比較、再実装検証 |
|
||||
| ワークスペースと観察 | 21 | セッション、証跡バンドル、ナビゲーション文脈、プロセス・成果物・関数比較、未解決事項の記録 |
|
||||
|
||||
## ロードマップ
|
||||
|
||||
+5
-3
@@ -17,6 +17,8 @@
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&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>
|
||||
|
||||
**[웹사이트(영문)](https://morluto.github.io/rea/) · [가이드](https://morluto.github.io/rea/guides/) · [DX-Ball 사례](https://morluto.github.io/rea/showcase/dx-ball/)**
|
||||
|
||||
[빠른 시작](#빠른-시작) · [현재 지원 범위](#현재-지원-범위) · [바이너리에서 동작까지](#바이너리에서-동작까지) · [조사 도구 카탈로그](#조사-도구-카탈로그) · [로드맵](#로드맵) · [작동 방식](#작동-방식)
|
||||
|
||||
<table aria-label="REA community">
|
||||
@@ -137,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는 별도 소프트웨어입니다. 데모에는 공급업체의 제한이 있지만 유료 라이선스가 필수는 아닙니다.
|
||||
@@ -152,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)를 참고하세요.
|
||||
|
||||
### 업데이트와 제거
|
||||
|
||||
@@ -213,7 +215,7 @@ REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이
|
||||
| 브라우저 관찰 | 9 | 페이지 구조, 네트워크 메타데이터, 스크립트, 소스 맵, WebMCP 탐색, 스크린샷, 캡처 비교 |
|
||||
| Electron 분석 | 5 | 렌더러 관찰, 정적 앱 매핑, 정적·런타임 결과 연결 |
|
||||
| JavaScript 런타임 | 2 | Node/Electron Inspector 대상 탐색, 스크립트 위치, 실행 컨텍스트 이벤트 |
|
||||
| 앱 워크플로 | 11 | 캡처한 웹 스크립트 내보내기, 계층 간 기능 추적, 빌드 비교, 히스토리 소스 매핑, 정적 반환 구조 비교, 재구현 검증 |
|
||||
| 앱 워크플로 | 13 | 캡처한 웹 스크립트 내보내기, 계층 간 기능 추적, 빌드 비교, 히스토리 소스 매핑, 정적 반환 구조 비교, 재구현 검증 |
|
||||
| 작업 공간과 관찰 | 21 | 세션, 근거 번들, 탐색 컨텍스트, 프로세스·아티팩트·함수 비교, 미해결 항목 기록 |
|
||||
|
||||
## 로드맵
|
||||
|
||||
+5
-3
@@ -17,6 +17,8 @@
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&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>
|
||||
|
||||
**[网站(英文)](https://morluto.github.io/rea/) · [使用指南](https://morluto.github.io/rea/guides/) · [DX-Ball 案例](https://morluto.github.io/rea/showcase/dx-ball/)**
|
||||
|
||||
[快速开始](#快速开始) · [当前状态](#当前状态) · [从二进制到行为](#从二进制到行为) · [调查工具目录](#调查工具目录) · [路线图](#路线图) · [工作原理](#工作原理)
|
||||
|
||||
<table aria-label="REA community">
|
||||
@@ -136,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 是独立软件;演示模式有厂商规定的限制,不要求购买许可证。
|
||||
@@ -151,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)。
|
||||
|
||||
### 更新与卸载
|
||||
|
||||
@@ -212,7 +214,7 @@ REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常
|
||||
| 浏览器观察 | 9 | 页面结构、网络元数据、脚本、来源映射、WebMCP 发现、截图与捕获比较 |
|
||||
| Electron 分析 | 5 | 渲染进程观察、静态应用映射、静态/运行时结果关联 |
|
||||
| JavaScript 运行时 | 2 | Node/Electron Inspector 目标发现、脚本位置与执行上下文事件 |
|
||||
| 应用工作流 | 11 | 捕获的网站脚本导出、跨层功能追踪、构建比较、历史源码映射、静态返回结构比较与重建验证 |
|
||||
| 应用工作流 | 13 | 捕获的网站脚本导出、跨层功能追踪、构建比较、历史源码映射、静态返回结构比较与重建验证 |
|
||||
| 工作区与观察 | 21 | 会话、证据包、导航上下文、进程/产物/函数比较与待解决问题跟踪 |
|
||||
|
||||
## 路线图
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
import com.atxx.jhmcp.*;
|
||||
import com.google.gson.*;
|
||||
import io.modelcontextprotocol.kotlin.sdk.server.*;
|
||||
import io.modelcontextprotocol.kotlin.sdk.types.*;
|
||||
import jadx.api.*;
|
||||
import jadx.core.dex.nodes.*;
|
||||
import java.io.PrintStream;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.stream.Collectors;
|
||||
import kotlin.Unit;
|
||||
import kotlin.coroutines.EmptyCoroutineContext;
|
||||
import kotlinx.coroutines.BuildersKt;
|
||||
import kotlinx.io.CoreKt;
|
||||
import kotlinx.io.JvmCoreKt;
|
||||
import kotlinx.serialization.json.JsonObject;
|
||||
|
||||
/** REA-owned metadata boundary for the unmodified jadx-headless-mcp 0.7.1 JAR. */
|
||||
class ReaJadxBridge {
|
||||
private static final Gson JSON = new Gson();
|
||||
private final SessionHolder holder;
|
||||
private JadxSession indexedSession;
|
||||
private Map<String, List<ClassSnapshot>> classes = Map.of();
|
||||
|
||||
private record MethodSnapshot(String name, String shortId, Map<String, Object> summary) {}
|
||||
private record ClassSnapshot(JavaClass node, List<MethodSnapshot> methods, Map<String, Object> summary) {}
|
||||
|
||||
private ReaJadxBridge(SessionHolder holder) { this.holder = holder; }
|
||||
|
||||
private static CallToolResult text(String value, boolean failed) {
|
||||
return new CallToolResult(List.of(new TextContent(value, null, null)), failed, null, null);
|
||||
}
|
||||
|
||||
private static CallToolResult json(Object value) { return text(JSON.toJson(value), false); }
|
||||
|
||||
private static String string(JsonObject args, String name) {
|
||||
JsonElement value = JsonParser.parseString(args.toString()).getAsJsonObject().get(name);
|
||||
if (value == null || !value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString())
|
||||
throw new IllegalArgumentException(name + " must be a string");
|
||||
return value.getAsString();
|
||||
}
|
||||
|
||||
private static int integer(JsonObject args, String name, int fallback) {
|
||||
JsonElement value = JsonParser.parseString(args.toString()).getAsJsonObject().get(name);
|
||||
if (value == null) return fallback;
|
||||
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isNumber())
|
||||
throw new IllegalArgumentException(name + " must be a nonnegative integer");
|
||||
try {
|
||||
int result = value.getAsBigDecimal().intValueExact();
|
||||
if (result < 0) throw new ArithmeticException();
|
||||
return result;
|
||||
} catch (ArithmeticException error) {
|
||||
throw new IllegalArgumentException(name + " must be a nonnegative integer", error);
|
||||
}
|
||||
}
|
||||
|
||||
private JadxSession session() {
|
||||
JadxSession current = holder.current();
|
||||
if (current == null) throw new IllegalArgumentException("No APK loaded");
|
||||
return current;
|
||||
}
|
||||
|
||||
private ClassSnapshot selected(JsonObject args) {
|
||||
JadxSession current = session();
|
||||
if (indexedSession != current) {
|
||||
Map<String, List<ClassSnapshot>> index = new LinkedHashMap<>();
|
||||
if (!current.getClasses().isEmpty()) {
|
||||
JadxDecompiler decompiler = current.getClasses().get(0).getClassNode().root().getDecompiler();
|
||||
// JavaClass.getMethods/getFields/getInnerClasses all load code. Raw nodes do not.
|
||||
// Snapshot every identity before code generation can rewrite aliases or members.
|
||||
for (JavaClass cls : decompiler.getClassesWithInners()) {
|
||||
ClassNode node = cls.getClassNode();
|
||||
List<MethodNode> ordered = new ArrayList<>(node.getMethods());
|
||||
ordered.sort(Comparator.comparing(MethodNode::getAlias));
|
||||
List<MethodSnapshot> methods = ordered.stream().map(method -> {
|
||||
String name = method.getAlias();
|
||||
String signature = name + "(" + method.getArgTypes().stream()
|
||||
.map(Object::toString).collect(Collectors.joining(", "))
|
||||
+ "): " + method.getReturnType().toString();
|
||||
return new MethodSnapshot(name, method.getMethodInfo().getShortId(), Map.of(
|
||||
"name", name, "signature", signature,
|
||||
"is_constructor", method.isConstructor(), "def_pos", 0));
|
||||
}).toList();
|
||||
List<Map<String, Object>> fields = node.getFields().stream()
|
||||
.sorted(Comparator.comparing(FieldNode::getAlias))
|
||||
.map(field -> Map.<String, Object>of("name", field.getAlias(),
|
||||
"type", field.getType().toString(), "def_pos", 0)).toList();
|
||||
List<String> inner = node.getInnerClasses().stream().map(ClassNode::getFullName).toList();
|
||||
Map<String, Object> summary = Map.of(
|
||||
"full_name", cls.getFullName(), "name", cls.getName(),
|
||||
"method_count", methods.size(), "field_count", fields.size(),
|
||||
"inner_class_count", inner.size(), "inner_classes", inner,
|
||||
"methods", methods.stream().map(MethodSnapshot::summary).toList(), "fields", fields);
|
||||
index.computeIfAbsent(cls.getFullName(), ignored -> new ArrayList<>())
|
||||
.add(new ClassSnapshot(cls, methods, summary));
|
||||
}
|
||||
}
|
||||
classes = Collections.unmodifiableMap(index);
|
||||
indexedSession = current;
|
||||
}
|
||||
String name = string(args, "class_name");
|
||||
List<ClassSnapshot> candidates = classes.get(name);
|
||||
if (candidates == null) throw new IllegalArgumentException("Exact class not found: " + name);
|
||||
if (candidates.size() != 1) throw new IllegalArgumentException("Ambiguous metadata class identity: " + name
|
||||
+ "; raw identities: " + candidates.stream().map(candidate -> candidate.node().getRawName()).toList());
|
||||
return candidates.get(0);
|
||||
}
|
||||
|
||||
private List<MethodSnapshot> methods(ClassSnapshot cls, JsonObject args) {
|
||||
String name = string(args, "method_name");
|
||||
List<MethodSnapshot> result = cls.methods().stream().filter(method -> method.name().equals(name)).toList();
|
||||
if (result.isEmpty()) throw new IllegalArgumentException("Method not found: " + name);
|
||||
return result;
|
||||
}
|
||||
|
||||
private JavaMethod method(ClassSnapshot cls, MethodSnapshot selected) {
|
||||
JavaMethod result = cls.node().searchMethodByShortId(selected.shortId());
|
||||
if (result == null) throw new IllegalArgumentException("Method identity no longer available: " + selected.shortId());
|
||||
return result;
|
||||
}
|
||||
|
||||
private CallToolResult handle(String name, JsonObject args) {
|
||||
if (name.equals("rea_jvm_status")) return json(Map.of(
|
||||
"engine_reported_version", BuildInfo.VERSION,
|
||||
"max_heap_bytes", Runtime.getRuntime().maxMemory(),
|
||||
"available_processors", Runtime.getRuntime().availableProcessors(),
|
||||
"java_version", System.getProperty("java.version"),
|
||||
"metadata_scope", "parsed_members"));
|
||||
JadxSession current = session();
|
||||
if (name.equals("list_classes")) {
|
||||
List<String> names = current.getClassFqns();
|
||||
int offset = integer(args, "offset", 0);
|
||||
int limit = integer(args, "limit", 200);
|
||||
int start = Math.min(offset, names.size());
|
||||
int end = (int) Math.min((long) start + limit, names.size());
|
||||
return json(Map.of("total", names.size(), "offset", offset, "limit", limit,
|
||||
"items", names.subList(start, end)));
|
||||
}
|
||||
ClassSnapshot cls = selected(args);
|
||||
if (name.equals("get_class_summary")) return json(cls.summary());
|
||||
if (name.equals("get_method_body")) {
|
||||
List<MethodSnapshot> overloads = methods(cls, args);
|
||||
int index = integer(args, "overload_index", 0);
|
||||
if (index >= overloads.size()) throw new IllegalArgumentException("overload_index out of range: " + index);
|
||||
JavaMethod method = method(cls, overloads.get(index));
|
||||
JadxSession.SmartCode code = current.getMethodBodySmart(method, current.getMaxSourceBytes(), true);
|
||||
return json(Map.of("class_name", cls.summary().get("full_name"),
|
||||
"method_name", overloads.get(index).name(), "full_name", method.getFullName(),
|
||||
"overload_index", index, "overload_count", overloads.size(),
|
||||
"mode", code.getKind(), "fell_back", code.getFellBack(),
|
||||
"markers", code.getMarkers(), "body", code.getText()));
|
||||
}
|
||||
List<JavaNode> uses;
|
||||
String target = string(args, "class_name");
|
||||
if (name.equals("get_xrefs_to_class")) uses = cls.node().getUseIn();
|
||||
else {
|
||||
List<MethodSnapshot> overloads = methods(cls, args);
|
||||
if (overloads.size() != 1) throw new IllegalArgumentException("Method references require a unique overload");
|
||||
target += "." + overloads.get(0).name();
|
||||
uses = method(cls, overloads.get(0)).getUseIn();
|
||||
}
|
||||
// REA consumes all references without materializing source line information.
|
||||
List<Map<String, Object>> items = uses.stream().map(node -> current.describeUsage(node, false)).toList();
|
||||
return json(Map.of("target", target, "total", uses.size(), "count", items.size(),
|
||||
"resolve_line", false, "items", items));
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
PrintStream protocolOut = System.out;
|
||||
System.setOut(System.err);
|
||||
SessionHolder holder = new SessionHolder(new SessionConfig(
|
||||
1048576, 0, 90000L, 1, List.of(), List.of(), 64, ResourceMode.FULL));
|
||||
ServerCapabilities capabilities = new ServerCapabilities(
|
||||
new ServerCapabilities.Tools(null), null, null, null, null, null, null, Map.of());
|
||||
Server server = new Server(new Implementation("rea-jadx-bridge", "1", null, null, null),
|
||||
new ServerOptions(capabilities), "", ignored -> Unit.INSTANCE);
|
||||
SessionToolsKt.registerSessionTools(server, holder);
|
||||
ManifestToolsKt.registerManifestTools(server, holder);
|
||||
ClassToolsKt.registerClassTools(server, holder);
|
||||
MethodToolsKt.registerMethodTools(server, holder);
|
||||
XrefToolsKt.registerXrefTools(server, holder);
|
||||
ReaJadxBridge bridge = new ReaJadxBridge(holder);
|
||||
Set<String> consumed = Set.of("load_apk", "get_app_info", "get_android_manifest",
|
||||
"list_classes", "get_class_summary", "get_method_body",
|
||||
"get_xrefs_to_class", "get_xrefs_to_method");
|
||||
for (String name : new ArrayList<>(server.getTools().keySet()))
|
||||
if (!consumed.contains(name)) server.removeTool(name);
|
||||
Tool diagnostics = new Tool("rea_jvm_status",
|
||||
new ToolSchema(null, new JsonObject(Map.of()), List.of(), null),
|
||||
"Observed worker JVM and metadata scope", null, null, null, null, null, null);
|
||||
server.addTool(diagnostics, (connection, request, continuation) -> bridge.handle("rea_jvm_status", request.getArguments()));
|
||||
for (String name : List.of("list_classes", "get_class_summary",
|
||||
"get_method_body", "get_xrefs_to_class", "get_xrefs_to_method")) {
|
||||
Tool tool = server.getTools().get(name).getTool();
|
||||
server.removeTool(name);
|
||||
List<String> fields = switch (name) {
|
||||
case "list_classes" -> List.of("offset", "limit");
|
||||
case "get_method_body" -> List.of("class_name", "method_name", "overload_index");
|
||||
case "get_xrefs_to_method" -> List.of("class_name", "method_name");
|
||||
default -> List.of("class_name");
|
||||
};
|
||||
Map<String, kotlinx.serialization.json.JsonElement> properties = new LinkedHashMap<>();
|
||||
for (String field : fields) properties.put(field, tool.getInputSchema().getProperties().get(field));
|
||||
String description = switch (name) {
|
||||
case "list_classes" -> "Indexed pages of all top-level class names, with offset and limit.";
|
||||
case "get_class_summary" -> "Parsed member metadata for one exact class name; no source generation.";
|
||||
case "get_method_body" -> "Source for an exact class and stable same-name overload index, with smali fallback.";
|
||||
default -> "Complete static references to an exact class or unique method; source lines are unresolved.";
|
||||
};
|
||||
Tool narrowed = new Tool(name, new ToolSchema(null, new JsonObject(properties),
|
||||
tool.getInputSchema().getRequired(), null), description, null, null, null, null, null, null);
|
||||
server.addTool(narrowed, (connection, request, continuation) -> {
|
||||
try { return bridge.handle(name, request.getArguments()); }
|
||||
catch (IllegalArgumentException error) { return text(error.getMessage(), true); }
|
||||
});
|
||||
}
|
||||
CountDownLatch done = new CountDownLatch(1);
|
||||
server.onClose(() -> { done.countDown(); return Unit.INSTANCE; });
|
||||
StdioServerTransport transport = new StdioServerTransport(
|
||||
CoreKt.buffered(JvmCoreKt.asSource(System.in)),
|
||||
CoreKt.buffered(JvmCoreKt.asSink(protocolOut)));
|
||||
transport.onClose(() -> { done.countDown(); return Unit.INSTANCE; });
|
||||
ServerSession connection = null;
|
||||
try {
|
||||
connection = BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
||||
(scope, continuation) -> server.createSession(transport, continuation));
|
||||
done.await();
|
||||
} finally {
|
||||
try {
|
||||
if (connection != null) {
|
||||
ServerSession connected = connection;
|
||||
// Unsubscribe the connection before the SDK joins its notification service.
|
||||
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
||||
(scope, continuation) -> connected.close(continuation));
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
||||
(scope, continuation) -> server.close(continuation));
|
||||
} finally {
|
||||
BuildersKt.runBlocking(EmptyCoroutineContext.INSTANCE,
|
||||
(scope, continuation) -> holder.unload(continuation));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
struct ChildBatch<Element> {
|
||||
let values: [Element]
|
||||
let complete: Bool
|
||||
}
|
||||
|
||||
func captureChildValues<Element>(requestedCount: Int, copy: () -> [Element]?) -> ChildBatch<Element> {
|
||||
guard requestedCount > 0 else { return ChildBatch(values: [], complete: true) }
|
||||
let returned = copy() ?? []
|
||||
let values = Array(returned.prefix(requestedCount))
|
||||
return ChildBatch(values: values, complete: values.count == requestedCount)
|
||||
}
|
||||
@@ -26,11 +26,12 @@ func childCount(_ element: AXUIElement) -> Int {
|
||||
var count = 0
|
||||
return AXUIElementGetAttributeValueCount(element, kAXChildrenAttribute as CFString, &count) == .success ? count : 0
|
||||
}
|
||||
func children(_ element: AXUIElement, limit: Int) -> [AXUIElement] {
|
||||
var value: CFArray?
|
||||
let count = min(limit, childCount(element))
|
||||
if count == 0 { return [] }
|
||||
return AXUIElementCopyAttributeValues(element, kAXChildrenAttribute as CFString, 0, count, &value) == .success ? value as? [AXUIElement] ?? [] : []
|
||||
func children(_ element: AXUIElement, count: Int) -> ChildBatch<AXUIElement> {
|
||||
captureChildValues(requestedCount: count) {
|
||||
var value: CFArray?
|
||||
guard AXUIElementCopyAttributeValues(element, kAXChildrenAttribute as CFString, 0, count, &value) == .success else { return nil }
|
||||
return value as? [AXUIElement]
|
||||
}
|
||||
}
|
||||
func text(_ element: AXUIElement, _ key: String) -> Any { (attribute(element, key) as? String) ?? NSNull() as Any }
|
||||
func windowBounds(_ element: AXUIElement) -> CGRect? {
|
||||
@@ -115,9 +116,12 @@ func observe(_ request: Request) async throws -> [String: Any] {
|
||||
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)
|
||||
let items = children(element, limit: count)
|
||||
let batch = children(element, count: count)
|
||||
if !batch.complete { truncated = true }
|
||||
if totalChildren > count { truncated = true }
|
||||
for index in (0..<count).reversed() { pending.append((items[index], path + [index])) }
|
||||
for (index, item) in batch.values.enumerated().reversed() {
|
||||
pending.append((item, path + [index]))
|
||||
}
|
||||
}
|
||||
}
|
||||
var screenshot: Any = NSNull()
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { defineConfig } from "vitepress";
|
||||
|
||||
export default defineConfig({
|
||||
title: "REA",
|
||||
description:
|
||||
"Reverse engineer binaries, applications, and runtime behavior with one CLI and MCP server.",
|
||||
lang: "en-US",
|
||||
base: "/rea/",
|
||||
themeConfig: {
|
||||
nav: [
|
||||
{ text: "Get started", link: "/installation" },
|
||||
{ text: "Reference", link: "/mcp-contracts" },
|
||||
{ text: "Roadmap", link: "/roadmap" },
|
||||
],
|
||||
sidebar: [
|
||||
{
|
||||
text: "Getting started",
|
||||
items: [
|
||||
{ text: "Installation and setup", link: "/installation" },
|
||||
{ text: "Agent prompts", link: "/mcp-prompts" },
|
||||
{ text: "Roadmap", link: "/roadmap" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Investigation guides",
|
||||
items: [
|
||||
{ text: "Native binaries", link: "/native-investigation" },
|
||||
{
|
||||
text: "JavaScript and Electron",
|
||||
link: "/javascript-artifact-reconstruction",
|
||||
},
|
||||
{
|
||||
text: "JavaScript workflows",
|
||||
link: "/javascript-application-workflows",
|
||||
},
|
||||
{ text: "Browser observation", link: "/browser-observation" },
|
||||
{ text: "Electron observation", link: "/electron-observation" },
|
||||
{ text: "Process capture", link: "/process-capture" },
|
||||
{ text: ".NET assemblies", link: "/managed-code-analysis" },
|
||||
{ text: "Android applications", link: "/android-analysis" },
|
||||
{ text: "Apple applications", link: "/apple-application-analysis" },
|
||||
{ text: "Firmware", link: "/firmware-analysis" },
|
||||
{
|
||||
text: "Reconstruction readiness",
|
||||
link: "/reconstruction-readiness",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Provider guides",
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: "IDA", link: "/ida-provider" },
|
||||
{ text: "Ghidra on Windows", link: "/windows-ghidra-p0" },
|
||||
{ text: "Ghidra DOS analysis", link: "/ghidra-dos" },
|
||||
{ text: "Ghidra NativeAOT", link: "/ghidra-nativeaot" },
|
||||
{ text: "Provider evaluation", link: "/provider-evaluation" },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: "Reference and development",
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: "MCP contracts", link: "/mcp-contracts" },
|
||||
{ text: "Tool design", link: "/tool-design" },
|
||||
{ text: "Testing", link: "/testing" },
|
||||
{ text: "Architecture decisions", link: "/adr/README" },
|
||||
],
|
||||
},
|
||||
],
|
||||
search: { provider: "local" },
|
||||
socialLinks: [
|
||||
{ icon: "github", link: "https://github.com/morluto/rea" },
|
||||
{ icon: "discord", link: "https://discord.gg/GkcryMnJDM" },
|
||||
],
|
||||
editLink: {
|
||||
pattern: "https://github.com/morluto/rea/edit/main/docs/:path",
|
||||
},
|
||||
footer: {
|
||||
message: "Released under the MIT License.",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -29,12 +29,12 @@ same bytes could therefore disagree because of language, compiler, loader,
|
||||
analyzer, or provider-version differences while appearing target-compatible.
|
||||
|
||||
The decision addressed the earlier representations of
|
||||
[`CompositeProvider`](../../src/application/CompositeProvider.ts),
|
||||
[runtime composition](../../src/application/runtime.ts),
|
||||
[`BinarySession`](../../src/application/BinarySession.ts),
|
||||
[`BinaryTarget`](../../src/domain/binaryTarget.ts),
|
||||
[analysis snapshot](../../src/domain/analysisSnapshot.ts), and
|
||||
[session lifecycle inputs](../../src/contracts/sessionLifecycleInputs.ts).
|
||||
[`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),
|
||||
[`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
|
||||
[session lifecycle inputs](https://github.com/morluto/rea/blob/main/src/contracts/sessionLifecycleInputs.ts).
|
||||
|
||||
The next deep provider must not weaken the existing product invariants:
|
||||
|
||||
|
||||
+42
-11
@@ -2,7 +2,7 @@
|
||||
|
||||
REA wraps an existing headless JADX engine through `src/android/`. Its source is
|
||||
kept as an unmodified, commit-pinned Git submodule; see
|
||||
[upstream provenance](../third_party/README.md). CLI and MCP use the same
|
||||
[upstream provenance](https://github.com/morluto/rea/blob/main/third_party/README.md). CLI and MCP use the same
|
||||
application workflow and return artifact-bound Evidence inline.
|
||||
|
||||
This family is present on repository main and in npm 4.1.0. Check the
|
||||
@@ -11,7 +11,9 @@ connected server's tool list before selecting it.
|
||||
|
||||
## Supply tools
|
||||
|
||||
Use **Java 17 or newer** and **jadx-headless-mcp 0.7.1**. REA does not install Java,
|
||||
Use a **full JDK 17 or newer** (including its compiler module) and
|
||||
**jadx-headless-mcp 0.7.1**. REA runs its packaged Java metadata bridge in
|
||||
source-file mode against the unmodified engine JAR. REA does not install Java,
|
||||
an Android SDK, JADX, a device service or an emulator. Set the path in the shell
|
||||
or MCP server environment:
|
||||
|
||||
@@ -21,8 +23,9 @@ export REA_JADX_MCP_JAR=/absolute/path/jadx-headless-mcp-0.7.1-all.jar
|
||||
export JAVA_HOME=/absolute/path/existing-jdk
|
||||
```
|
||||
|
||||
Linux is verified with the public Appium ApiDemos fixture. The POSIX adapter can
|
||||
run on macOS, but that host has not undergone real Android verification.
|
||||
The current metadata bridge is verified on macOS arm64 with OpenJDK 21 and the
|
||||
public Appium ApiDemos fixture. The POSIX adapter also supports Linux; the new
|
||||
bridge has not yet undergone real verification on Linux/JDK 25.
|
||||
Windows is unsupported for this provider's owned stdio process boundary.
|
||||
|
||||
## Answer an analyst question
|
||||
@@ -72,7 +75,10 @@ selected overload. Native/abstract methods with no decompiled body report
|
||||
Signature verification is explicitly `not_performed`.
|
||||
- Class search consumes all pages and checks their counts. An empty `query`
|
||||
inventories all class names; class names are decompiler representations.
|
||||
- Class inventories return display signatures, fields and inner class names.
|
||||
- Class inventories read parsed metadata without generating source and return
|
||||
full reported display types, fields and inner class names. Synthetic members
|
||||
that code generation hides can appear in the inventory; it stays consistent
|
||||
after method decompilation within the retained session.
|
||||
Exact DEX descriptors are unknown. Overload indices are tied to this artifact
|
||||
and engine, not stable identities across different builds.
|
||||
- Method inspection reports Java or smali, fallback markers and complete/partial
|
||||
@@ -109,23 +115,48 @@ runtime calls.
|
||||
|
||||
## Resource and lifecycle limits
|
||||
|
||||
Each request creates private, immutable APK and JAR copies. Their workspace and
|
||||
owned process group are cleaned on success, failure, timeout and cancellation.
|
||||
REA retains one serialized session with private, immutable APK, engine JAR and
|
||||
metadata-bridge copies. Matching requests reuse the loaded engine. APK/engine
|
||||
digests and JVM configuration are checked before reuse; changed inputs retire
|
||||
the previous session. Sessions are cleaned after 60 seconds of inactivity, on
|
||||
target/configuration changes, failure, timeout, cancellation, MCP disconnect or
|
||||
server shutdown. One-shot CLI commands always join cleanup before returning.
|
||||
REA never writes to the original APK or launches target code.
|
||||
|
||||
If process cleanup cannot be confirmed, REA retains the workspace and reports its
|
||||
location. That provider instance blocks subsequent engine launches until the
|
||||
caller resolves the reported resources and starts a fresh instance.
|
||||
|
||||
The provider serializes requests within each REA instance, uses one worker and
|
||||
sets JVM maximum heap to **512 MiB**. Heap is not a total RSS cap: the JVM also
|
||||
The provider uses one JADX analysis worker independently of JVM processor and
|
||||
garbage-collector settings. REA supplies no default heap or visible-CPU override;
|
||||
Java uses its normal ergonomics and inherits caller JVM settings. Optional
|
||||
provider settings add explicit JVM arguments:
|
||||
|
||||
```sh
|
||||
export REA_JADX_HEAP_MIB=8192
|
||||
# Optional, independent of the single JADX worker:
|
||||
export REA_JADX_ACTIVE_PROCESSOR_COUNT=4
|
||||
# Or select settings through Java's standard environment options:
|
||||
export JAVA_TOOL_OPTIONS=-Xmx8g
|
||||
```
|
||||
|
||||
Both REA settings must be positive safe integers; processor counts must also fit
|
||||
the JVM's signed 32-bit range. REA preserves
|
||||
`JAVA_TOOL_OPTIONS`, `JDK_JAVA_OPTIONS` and `_JAVA_OPTIONS` rather than replacing
|
||||
them. Java's standard precedence applies: explicit command-line heap settings
|
||||
override `JAVA_TOOL_OPTIONS`, while `_JAVA_OPTIONS` can override command-line
|
||||
settings. Evidence reports the worker's observed `Runtime.maxMemory()` rounded
|
||||
down to MiB and retains exact bytes, Java version and visible processors in its
|
||||
raw runtime response. The observed allocatable heap can differ from configured
|
||||
`-Xmx`, especially with Serial GC. Heap is not a total RSS cap: the JVM also
|
||||
uses native memory. Independent REA instances have independent queues. Whole
|
||||
operations have a **120-second** execution deadline after reaching the queue's
|
||||
front; upstream decompilation is limited to **90 seconds**.
|
||||
|
||||
Decoded manifest and method text have a **1 MiB** upstream byte budget and report
|
||||
truncation. A protocol frame above **8 MiB**, or cumulative stdout/stderr above
|
||||
**32 MiB**, fails with a diagnostic instead of returning an incomplete inventory
|
||||
truncation. Class-name pages use indexed slices of up to 4,096 names without changing the
|
||||
complete, case-sensitive search contract. A protocol frame above **8 MiB**, or combined stdout/stderr above
|
||||
**32 MiB per operation**, fails with a diagnostic instead of returning an incomplete inventory
|
||||
as complete. These are memory-safety boundaries, not undocumented result caps.
|
||||
|
||||
## Real verification
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Apple Application Inventory Projection
|
||||
|
||||
REA can project one or more authenticated `inventory_artifact` Evidence records
|
||||
for the same IPA into an Apple application inventory. The projection is
|
||||
execution-free: it reports exact paths, content hashes, detected formats,
|
||||
runtime-family hints, and path-based bridge hypotheses. It does not parse plist
|
||||
or CMS semantics or claim observed runtime calls.
|
||||
for the same IPA, macOS `.app` directory, ZIP, or DMG into an Apple application
|
||||
inventory. The projection is execution-free: it reports exact paths, content
|
||||
hashes, detected formats, bundle anatomy, runtime-family hints, and path-based
|
||||
bridge hypotheses. It does not parse plist or CMS semantics or claim observed
|
||||
runtime calls.
|
||||
|
||||
```sh
|
||||
rea project-apple-application-graph '{"inventory_evidence":[<inventory_artifact Evidence>]}'
|
||||
@@ -29,3 +30,83 @@ Coverage is `complete-within-inventory` when the supplied pages reconstruct the
|
||||
complete authenticated inventory, and `partial` otherwise. A partial result
|
||||
states that absence is unknown. Component and bridge-candidate arrays are still
|
||||
the complete projection of the inventory that was supplied.
|
||||
|
||||
## Application roots
|
||||
|
||||
| Inventory root | Application root |
|
||||
| -------------------------------------- | --------------------------------------------------- |
|
||||
| IPA | Each `Payload/<Name>.app` |
|
||||
| `.app` directory | `.`, the inventoried bundle itself |
|
||||
| Directory, ZIP, or DMG containing apps | Each outermost `<Name>.app` followed by `Contents/` |
|
||||
|
||||
ZIP archives made with `ditto -c -k --keepParent` report `<Name>.app`. A DMG
|
||||
reports `<image>.dmg/<Volume>/<Name>.app`; DMG child inventory needs the native
|
||||
macOS mounting adapter. When a DMG has only its root identity, the projection is
|
||||
`partial` and says that bundle absence is unknown. Other inventory formats, such
|
||||
as APK or ASAR, are rejected as input errors.
|
||||
|
||||
An inventoried directory whose name ends in `.app` is its own root even when its
|
||||
inventory is partial or lacks `Contents/`. For directory, ZIP, and DMG
|
||||
inventories, components outside every application root are not attributed to
|
||||
the application. This covers installers, disk-image extras, and AppleDouble
|
||||
sidecars, which therefore stay out of runtime families and bridge candidates. A
|
||||
limitation reports how many inventoried entries were left out. IPA projection
|
||||
still lists every archive component, including `SwiftSupport/`.
|
||||
|
||||
`platforms` lists `macos` for application roots with `Contents/`. It lists `ios`
|
||||
for shallow application roots that have an `Info.plist`. An application root
|
||||
with neither, such as an empty inventory, has no platform.
|
||||
|
||||
## macOS bundle anatomy
|
||||
|
||||
`bundles` lists each application root and every nested bundle under it. Each
|
||||
bundle reports its `parent_path`, `layout`, `info_plist_path`,
|
||||
`executable_candidates`, and `signing_paths`.
|
||||
|
||||
| Layout | Meaning | Content directory |
|
||||
| --------------------- | --------------- | --------------------------------------------- |
|
||||
| `macos-deep` | Has `Contents/` | `Contents/`, executables in `Contents/MacOS/` |
|
||||
| `versioned-framework` | Has `Versions/` | Each real `Versions/<version>/` |
|
||||
| `shallow` | Neither | The bundle directory itself |
|
||||
|
||||
Roles are path conventions (`role_basis: "path-convention"`):
|
||||
|
||||
| Role | Path |
|
||||
| -------------------- | --------------------------------------------------------------- |
|
||||
| `app-extension` | `*.appex` |
|
||||
| `xpc-service` | `*.xpc`, including XPC services inside frameworks |
|
||||
| `framework` | `*.framework` |
|
||||
| `system-extension` | `*.systemextension` |
|
||||
| `driver-extension` | `*.dext` |
|
||||
| `plug-in` | `PlugIns/*.bundle`, `*.plugin`, `*.qlgenerator`, `*.mdimporter` |
|
||||
| `resource-bundle` | `Resources/*.bundle`, such as SwiftPM resource bundles |
|
||||
| `bundle` | Any other `*.bundle` |
|
||||
| `login-item` | `Contents/Library/LoginItems/*.app` |
|
||||
| `helper-application` | `Contents/Helpers/*.app` |
|
||||
| `nested-application` | Any other nested `*.app` |
|
||||
|
||||
The components also list:
|
||||
|
||||
- `privileged_helpers`: files directly in `Contents/Library/LaunchServices/`,
|
||||
where SMJobBless helpers live;
|
||||
- `launchd_plists`: `.plist` files directly in `Contents/Library/LaunchAgents/`
|
||||
(`domain: "agent"`) or `Contents/Library/LaunchDaemons/` (`domain: "daemon"`),
|
||||
as used by SMAppService;
|
||||
- `helpers`: files directly in `Contents/Helpers/`.
|
||||
|
||||
Symlinks are reported by path in `symlinks`. Their targets are not inventoried,
|
||||
so a versioned framework is described through its real `Versions/<version>/`
|
||||
directories rather than `Versions/Current`. A framework's `info_plist_path` is
|
||||
`null`, with a limitation explaining why, when it has more than one real
|
||||
`Versions/<version>/` directory (even if only one of them holds a plist,
|
||||
because `Versions/Current` decides which one applies) or when its single
|
||||
version directory has no `Resources/Info.plist`.
|
||||
|
||||
AppleDouble sidecar files (`._*` and `__MACOSX/`) are inventory facts, but they
|
||||
describe neighbouring files. They are excluded from roots, bundle roles, and the
|
||||
anatomy components.
|
||||
|
||||
The projection does not read plists. Use `inspect_plist` on a bundle's
|
||||
`info_plist_path` or a launchd plist to read `CFBundleExecutable`, identifiers,
|
||||
`SMPrivilegedExecutables`, `BundleProgram`, URL schemes, and usage descriptions.
|
||||
Use `inspect_signature` on an executable candidate for code-signing claims.
|
||||
|
||||
@@ -27,7 +27,9 @@ 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 script adapters and publication"]
|
||||
BROWSER["src/browser/<br/>CDP / Playwright capture<br/>captured scripts and native module resolution"]
|
||||
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"]
|
||||
FIRMWARE["src/firmware/<br/>Binwalk / Unblob adapters<br/>owned processes, report normalization, private extraction"]
|
||||
ANDROID["src/android/<br/>Static APK adapter<br/>owned SDK stdio, pinned headless JADX, normalization"]
|
||||
@@ -94,6 +96,12 @@ graph TD
|
||||
SERVER --> CONTRACTS
|
||||
SERVER --> APP
|
||||
APP --> BROWSER
|
||||
APP -->|source-map codec port| JS_CODEC
|
||||
JS_CODEC --> PROCESS
|
||||
JS_CODEC --> DOMAIN
|
||||
APP -->|runtime observation port| INSPECTOR
|
||||
INSPECTOR -->|shared CDP and file-location helpers| BROWSER
|
||||
INSPECTOR --> DOMAIN
|
||||
APP --> DOTNET
|
||||
DOTNET --> DOMAIN
|
||||
HOPPER --> HOPPER_BRIDGE
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
Captured Debugger sources can be exported for REA's existing static JavaScript
|
||||
analysis with `export_web_scripts`. See [captured website scripts](website-script-export.md)
|
||||
for the capture options, source mappings, and browser/local resolution limits.
|
||||
Use [captured module relationships](website-module-trace.md) to resolve one
|
||||
exported script's native imports under an explicit URL/import-map context.
|
||||
|
||||
REA can attach to a user-owned Chrome-family browser through the Chrome DevTools Protocol (CDP) and produce bounded Evidence about an existing page. This is a passive reverse-engineering capability, not a general browser automation or remote-control surface.
|
||||
|
||||
|
||||
@@ -167,6 +167,24 @@ behavior. Snapshots still require an active target and concrete analysis profile
|
||||
extracting investigation ownership does not make them a target-free record store.
|
||||
#721 remains responsible for any future lifetime-policy change.
|
||||
|
||||
## Inspector adapter ownership
|
||||
|
||||
`src/inspector/` owns V8 Inspector discovery, passive capture, script/target
|
||||
location interpretation and its lightweight provider identity. The composition
|
||||
factory and optional loader use this owner. Browser CDP connection, endpoint/value
|
||||
and Electron file-location helpers remain deliberate shared adapter utilities at
|
||||
their existing paths; no provider wire protocol moves into domain or process.
|
||||
|
||||
Loopback fixtures and producer boundaries now live under
|
||||
`tests/fixtures/inspector/` and `tests/boundary/inspector/`. The socket-backed
|
||||
unresolved-target reconciliation test moves from composition into the forked
|
||||
boundary lane with its original assertions. Adapter tests retain fork isolation.
|
||||
`npm run verify:inspector` keeps its public development entrypoint and delegates
|
||||
to `scripts/verify/inspector/runtime-observation.mjs`, using the real Node fixture
|
||||
under `tests/conformance/inspector/`. Catalog imports, source guards and test
|
||||
discovery follow the new paths. Observation authority, lifecycle, producer
|
||||
interpretation and caller-visible contracts stay unchanged.
|
||||
|
||||
## Auxiliary source declaration pilot
|
||||
|
||||
Artifact, macOS native and managed static metadata now belongs to each adapter's
|
||||
@@ -189,3 +207,152 @@ All observation registrars resolve contracts by name. An isolated compiled-proce
|
||||
test reverses presentation arrays and performs actual MCP SDK calls to verify
|
||||
advertised schemas and handler meaning. Reordering metadata must not select a
|
||||
different operation, schema, provider or deep-provider priority.
|
||||
|
||||
## Android layer and verification ownership
|
||||
|
||||
Android workflows and their existing typed port live in `src/application/android/`,
|
||||
pure APK/inventory semantics in `src/domain/android/`, and the named inspection
|
||||
contracts in `src/contracts/android/`. The headless JADX producer remains in
|
||||
`src/android/`; `src/composition/android.ts` supplies its fresh factory to both
|
||||
caller adapters. Public names, result meaning and JVM/queue/cleanup limits are
|
||||
unchanged. Inventory-only application projection remains execution-free.
|
||||
|
||||
The real lane and signal-cleanup helper belong to `scripts/verify/android/`; its
|
||||
explicit downloader and fixed manifest belong to `scripts/fixtures/android/`.
|
||||
`verify:android` and `fixtures:android` keep their command names and repository-root
|
||||
`_reference/apk-integration/` fixture default. Synthetic producer fixtures stay in
|
||||
`tests/fixtures/android/`, with matching boundary and composition ownership. Real
|
||||
JADX CLI/MCP parity requires the fixed APK and existing audited JAR; synthetic
|
||||
protocol/cancellation success does not establish that engine or an unverified host.
|
||||
|
||||
## Deep-provider source declarations
|
||||
|
||||
Hopper, Ghidra and IDA declare their implemented analyst operations in their
|
||||
own `*ProviderCapabilities.ts` modules. Hopper's inventory is typed against the
|
||||
existing direct-operation contracts; Ghidra reuses its inventory/function adapter
|
||||
operations; IDA retains its read-only operation family. Frozen operation arrays
|
||||
feed the existing capability builders without importing generated catalog data.
|
||||
|
||||
The catalog generator reads these source declarations and joins operation names
|
||||
to canonical `TOOL_CONTRACTS`. A missing contract identifies the declaring provider
|
||||
and operation. Public schemas and descriptions remain owned by contracts; the
|
||||
declarations describe implementation coverage. Provider adapters must not consume
|
||||
the generated MCP catalog that is derived from them. The resolved module guard
|
||||
checks this edge while admitting canonical source contracts, generated package
|
||||
metadata and the existing application snapshot-cache consumer.
|
||||
|
||||
Exact provider effects and lifecycle differences remain in their adapters:
|
||||
Hopper's GUI and mutation effects, Ghidra's experimental Windows authority and
|
||||
mutation restrictions, and IDA's attached/headless effects and live observations.
|
||||
This source increment introduces no common provider lifecycle or new interface.
|
||||
|
||||
## JavaScript application ownership
|
||||
|
||||
`src/application/javascript/` owns JavaScript/Electron artifact analysis and
|
||||
graph projection, feature/version/export/source comparisons, runtime Evidence
|
||||
and reconciliation, observation ports, and source recovery workflows. Pure
|
||||
analysis and public schemas keep their existing domain/contract ownership;
|
||||
shared artifact inventory, investigation records and execution types remain
|
||||
shared. CLI, MCP and production factories import the owning workflow directly.
|
||||
|
||||
`src/domain/javascript/` owns static parsing and semantic IR, application and
|
||||
semantic graphs, Electron facts, runtime reconciliation, recovery result
|
||||
semantics, and source-to-bundle comparisons. Shared Evidence, digests, ordering,
|
||||
comparison semantics and historical-source graphs stay at their existing owners.
|
||||
Native handoffs link exact-subject Evidence without selecting or executing a
|
||||
provider.
|
||||
|
||||
`src/contracts/javascript/` owns the JavaScript/Electron observation and recovery
|
||||
contracts, JavaScript-specific workflow requests, and their examples. The shared
|
||||
`applicationToolContracts.ts` aggregate stays at the contract root because it also
|
||||
owns reconstruction and Android/Apple projection operations. Canonical tool
|
||||
aggregation, named handler binding and SDK schema presentation remain unchanged.
|
||||
Catalog source paths, compiled verifier/SDK fixtures and recovery CI filters
|
||||
follow the domain/contract owners.
|
||||
|
||||
`scripts/verify/javascript/` groups application-workflow, runtime-reconciliation,
|
||||
large-digest and real-recovery entrypoints. Their existing npm commands retain
|
||||
their invocation semantics. Recovery fixture compilation lives beside its
|
||||
unchanged sources under `scripts/fixtures/javascript-recovery/`. Its default
|
||||
REA entrypoint still resolves to `scripts/rea.mjs`. Managed conformance imports
|
||||
the relocated compiled workflow; owning CI source filters follow the new paths.
|
||||
|
||||
`src/artifacts/javascript/` owns directory/ASAR reader construction and relevant
|
||||
file acquisition, including root/nested byte verification, UTF-8 diagnostics and
|
||||
nested reader cleanup. Its narrow factory returns the existing `ArtifactReader`;
|
||||
the application retains input admission, inventory, graph construction and its
|
||||
original root-reader `finally` close. Pure file facts live in
|
||||
`src/domain/javascript/javascriptArtifactFiles.ts`, and the shared immutable
|
||||
inventory snapshot lives in `src/domain/artifactInventorySnapshot.ts`. Inventory
|
||||
options and integrity policy remain application-owned. Shared stream hashing
|
||||
lives in `src/artifacts/ArtifactHash.ts`, preserving complete byte counts and the
|
||||
bounded classification prefix. Acquisition/hash guards reject imports from
|
||||
application, composition or caller adapters; JavaScript workflows use the factory
|
||||
instead of importing concrete ASAR/directory reader classes.
|
||||
|
||||
ZIP, native-DMG and Mach-O inventory reader selection keep their existing
|
||||
lifecycles. Runtime ports retain their distinct Inspector, passive Electron and
|
||||
active Electron effects; shipped Electron hooks remain at their existing
|
||||
provider boundary.
|
||||
|
||||
## Managed capability ownership
|
||||
|
||||
`src/domain/managed/` owns managed artifact and member semantics, reconstruction
|
||||
import, native-boundary verification and application graph projection.
|
||||
`src/application/managed/` owns their shared Evidence workflows, and
|
||||
`src/contracts/managed/` owns their named contracts and examples. Producer
|
||||
metadata/IL parsing stays in `src/dotnet/`; target resolution, Evidence, ordering
|
||||
and investigation records retain their shared owners. Path-based member
|
||||
comparison preserves its actual target/byte-digest admission and existing parser
|
||||
helper.
|
||||
|
||||
`scripts/verify/managed/` groups the portable conformance entrypoint and its
|
||||
support, manifest, oracle and completion-report helpers. Its PE byte fixture
|
||||
builder and declaration live in `scripts/fixtures/managed/`, shared with the
|
||||
installed-package lane and existing managed conformance tests. `verify:managed`
|
||||
keeps its command name and optional caller-cwd manifest resolution. Compiled
|
||||
catalog and generator imports follow the owners; document-check cache inputs
|
||||
include the relocated runtime verifier and fixture files.
|
||||
|
||||
Portable byte fixtures, optional operator-local manifests, actual ILSpy oracles
|
||||
and real Ghidra NativeAOT checks retain their distinct proof levels and
|
||||
prerequisites. The existing NativeAOT fixture/engine lane keeps its owner. This
|
||||
move preserves implementations, Evidence identities and generated commitments.
|
||||
|
||||
## Binary application ownership
|
||||
|
||||
`src/application/binary/` owns the active session, deep-provider registry and
|
||||
evaluation, operation routing, disjoint provider composition, lazy auxiliary
|
||||
clients, cancellation and client cleanup. Its records facade and snapshot
|
||||
cache/files remain bound to the exact target and analysis profile. Investigation
|
||||
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.
|
||||
|
||||
The three colocated application tests move with their owner. Composition cases
|
||||
remain in `tests/composition/analysis-sessions/`; filesystem and SDK cases retain
|
||||
their boundary lanes. The shared `tests/fixtures/binarySession.ts` factory supports
|
||||
several capability families. Existing recursive application globs discover the
|
||||
moved tests; bridge assets and real-verifier entrypoints remain at their owners.
|
||||
|
||||
## Firmware layer and verification ownership
|
||||
|
||||
Firmware workflows and their existing typed port live in
|
||||
`src/application/firmware/`, pure request/result semantics in
|
||||
`src/domain/firmware/`, and named contracts in `src/contracts/firmware/`.
|
||||
`src/firmware/` keeps producer commands, report parsing, source copies, publication
|
||||
and cleanup; `src/composition/firmware.ts` supplies a fresh typed factory.
|
||||
|
||||
The real lane belongs to `scripts/verify/firmware/analysis.mjs`; the fixture runner
|
||||
and its unchanged Python producer belong together in `scripts/fixtures/firmware/`.
|
||||
The npm command names and selected `REA_FIRMWARE_FIXTURE_ROOT` are unchanged. The
|
||||
runner retains its working-directory-relative default; the verifier retains its
|
||||
repository-root default. Both point to `_reference/firmware-integration/generated`
|
||||
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.
|
||||
|
||||
@@ -38,7 +38,7 @@ git submodule update --init third_party/binwalk third_party/unblob
|
||||
```
|
||||
|
||||
Normal REA builds and npm packages do not include or build these sources or
|
||||
engine binaries. See [upstream provenance](../third_party/README.md).
|
||||
engine binaries. See [upstream provenance](https://github.com/morluto/rea/blob/main/third_party/README.md).
|
||||
|
||||
## CLI and MCP
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: REA
|
||||
text: Reverse Engineer Anything
|
||||
tagline: Understand binaries, applications, and runtime behavior with one CLI and MCP server.
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Get started
|
||||
link: /installation
|
||||
- theme: alt
|
||||
text: View on GitHub
|
||||
link: https://github.com/morluto/rea
|
||||
|
||||
features:
|
||||
- title: Investigate with your agent
|
||||
details: Connect REA to your coding agent, describe the app or feature you want to understand, and follow the evidence.
|
||||
link: /mcp-prompts
|
||||
- title: Inspect across formats
|
||||
details: Explore native binaries, JavaScript and Electron apps, .NET assemblies, Android packages, firmware, and websites.
|
||||
link: /installation
|
||||
- title: Keep the evidence
|
||||
details: Analysis runs locally and returns observations, limitations, and unknowns through the CLI and MCP.
|
||||
link: /mcp-contracts
|
||||
---
|
||||
|
||||
## Start with one command
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
Choose your agent, review the planned changes, and approve setup. Native binary
|
||||
analysis can use an existing Hopper, Ghidra, or IDA installation; static
|
||||
JavaScript analysis needs no native analysis engine.
|
||||
|
||||
For a first result from an extracted JavaScript application or ASAR:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
See [installation and setup](./installation.md) for requirements and configuration.
|
||||
|
||||

|
||||
@@ -90,7 +90,7 @@ npx skills add morluto/rea --skill reverse-engineer-anything
|
||||
|
||||
This installs agent instructions and bundled references, not REA MCP
|
||||
registration or analysis engines. Follow the skill's
|
||||
[conditional connection guide](../skills/reverse-engineer-anything/SKILL.md#connect-only-when-needed).
|
||||
[conditional connection guide](https://github.com/morluto/rea/blob/main/skills/reverse-engineer-anything/SKILL.md#connect-only-when-needed).
|
||||
Working tools can be used immediately. If tools are missing, inspect the current
|
||||
client's registration with `doctor --client codex --json` (substitute its client
|
||||
ID), then plan repairs with `setup --client codex --dry-run --json`. Show and
|
||||
@@ -110,7 +110,7 @@ npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --
|
||||
It returns the complete Evidence record directly and requires no native engine.
|
||||
Provider failures in doctor do not prevent unrelated target-free tools. To check
|
||||
readiness for one task instead of auditing every integration, see
|
||||
[Check readiness for the task at hand](../README.md#check-readiness-for-the-task-at-hand).
|
||||
[Check readiness for the task at hand](https://github.com/morluto/rea/blob/main/README.md#check-readiness-for-the-task-at-hand).
|
||||
|
||||
## Supported agents
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ part of the current tool set.
|
||||
|
||||
This guide describes the implementation and verification of
|
||||
[ADR-0003](adr/0003-managed-code-evidence-and-provider-boundary.md). The canonical
|
||||
tool inventory is [`product-catalog.json`](product-catalog.json).
|
||||
tool inventory is [`product-catalog.json`](https://github.com/morluto/rea/blob/main/docs/product-catalog.json).
|
||||
|
||||
## Shipped scope
|
||||
|
||||
@@ -284,7 +284,13 @@ Cross-version matching first prefers exact CIL/signature identity. When that
|
||||
does not match, it pairs an exact declared type, method name, and raw signature
|
||||
before trying structural body shape. The exact-signature key uses names only
|
||||
as part of that full tuple; names alone never select a pair. Duplicate tuples
|
||||
remain ambiguous rather than being paired by token order.
|
||||
remain ambiguous rather than being paired by token order. Fields follow the
|
||||
same exact tiers. The raw signature is exact whether or not REA decoded it, so
|
||||
undecoded signatures pair by that tuple too, but they never enter structural
|
||||
rounds. An unmatched member is therefore reported as `unknown`, not added or
|
||||
removed, when its own signature was not decoded or when the other side has an
|
||||
unpaired member, one-sided or ambiguous, with the same declared type and name
|
||||
whose signature was not decoded.
|
||||
|
||||
For a matched method, an unavailable or partial body makes body-shape facets
|
||||
unknown while preserving observed signature differences. Structural identity
|
||||
|
||||
@@ -30,8 +30,8 @@ explicit limitation; architecture inventory locations remain available.
|
||||
connections and controller/class names. `verify:interface-builder` compiles
|
||||
a source-owned AppKit XIB with `ibtool`. Storyboards require an installed
|
||||
iOS platform; unsupported archive forms remain explicit.
|
||||
- `inspect_native_dispatch_metadata` / `rea inspect-native-dispatch-metadata
|
||||
<app-or-binary>` prefers a validated macOS Mach-O byte reader. It decodes
|
||||
- `inspect_native_dispatch_metadata` /
|
||||
`rea inspect-native-dispatch-metadata <app-or-binary>` prefers a validated macOS Mach-O byte reader. It decodes
|
||||
64-bit little-endian Objective-C class/metaclass records, superclass pointers,
|
||||
absolute/relative method entries, ivar offsets/sizes/alignment and protocol
|
||||
declarations. It also decodes simple Swift conformances, static synchronous
|
||||
@@ -39,9 +39,20 @@ explicit limitation; architecture inventory locations remain available.
|
||||
vtable descriptors. Records include exact virtual addresses/file offsets and
|
||||
artifact evidence. Vtable indexes are metadata word offsets; witness indexes
|
||||
start after the conformance header. Names unavailable in metadata remain null.
|
||||
Chained fixups, external bindings, authenticated pointers, categories,
|
||||
properties, generic/resilient/async/coroutine tables and inherited overrides
|
||||
have explicit unsupported or partial coverage. Other providers retain the
|
||||
Pointers are decoded through the image's fixups:
|
||||
- `LC_DYLD_CHAINED_FIXUPS` rebases and binds, including the authenticated
|
||||
arm64e formats;
|
||||
- legacy `LC_DYLD_INFO` bind opcodes.
|
||||
|
||||
So an external superclass resolves from its `_OBJC_CLASS_$_` bind (for
|
||||
example `NSObject`). The `pointer_fixups` coverage facet names the mechanism.
|
||||
Class properties, with parsed attributes, and `__objc_catlist` categories are
|
||||
also decoded. Categories record the extended class, local or external, plus
|
||||
their methods, protocols and properties; category methods appear as
|
||||
implementations with a `category`. Swift field-offset globals that are only
|
||||
initialized at runtime stay unresolved. Generic, resilient, async and
|
||||
coroutine tables, and inherited overrides, have explicit unsupported or
|
||||
partial coverage. Other providers retain the
|
||||
existing symbol-based inventory with its narrower coverage.
|
||||
|
||||
## Native instruction, call and type primitives
|
||||
@@ -165,8 +176,10 @@ GUI authority, and REA never falls back automatically to Hopper.
|
||||
instruction/call facts, value dependencies and process/project cleanup.
|
||||
- `npm run verify:ghidra:aarch64-jump-table`: optimized ELF and byte/halfword
|
||||
relative tables, plus ARM64 Mach-O on an ARM64 macOS host.
|
||||
- `npm run verify:apple-dispatch`: source-built Objective-C protocols/classes and
|
||||
Swift conformances/vtables, repeated after stripping local symbols.
|
||||
- `npm run verify:apple-dispatch`: source-built Objective-C protocols, classes,
|
||||
properties and categories, and Swift conformances and vtables. Each is linked
|
||||
with legacy `LC_DYLD_INFO` and with chained fixups (and as arm64e on Apple
|
||||
silicon), then repeated after stripping local symbols.
|
||||
- `npm run verify:native-ui`: one source-owned fixture window, successful
|
||||
selected-window capture and actions, changed-target rejection, and cleanup.
|
||||
Missing OS permissions fail this lane.
|
||||
|
||||
+19
-11
@@ -7,10 +7,10 @@
|
||||
"client": "2.3.1",
|
||||
"core": "2.3.1"
|
||||
},
|
||||
"skill_version": "26"
|
||||
"skill_version": "27"
|
||||
},
|
||||
"tools": {
|
||||
"total": 129,
|
||||
"total": 131,
|
||||
"families": [
|
||||
{
|
||||
"id": "direct",
|
||||
@@ -179,7 +179,7 @@
|
||||
{
|
||||
"id": "application",
|
||||
"surface": "application-workflow",
|
||||
"count": 11,
|
||||
"count": 13,
|
||||
"tools": [
|
||||
"build_reconstruction_obligation_ledger",
|
||||
"compare_application_versions",
|
||||
@@ -191,7 +191,9 @@
|
||||
"project_apple_application_graph",
|
||||
"recover_javascript_sources",
|
||||
"trace_application_feature",
|
||||
"trace_javascript_semantics"
|
||||
"trace_javascript_semantics",
|
||||
"trace_web_module_imports",
|
||||
"trace_web_source_location"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -470,7 +472,11 @@
|
||||
"id": "rea-web-script-export",
|
||||
"name": "REA captured script export",
|
||||
"version": "1",
|
||||
"capabilities": ["export_web_scripts"]
|
||||
"capabilities": [
|
||||
"export_web_scripts",
|
||||
"trace_web_module_imports",
|
||||
"trace_web_source_location"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "unblob",
|
||||
@@ -560,7 +566,7 @@
|
||||
}
|
||||
],
|
||||
"cli": {
|
||||
"primary_count": 84,
|
||||
"primary_count": 86,
|
||||
"commands": [
|
||||
"address-to-file-offset",
|
||||
"analyze",
|
||||
@@ -642,6 +648,8 @@
|
||||
"trace-javascript-semantics",
|
||||
"trace-native-ui-action",
|
||||
"trace-native-values",
|
||||
"trace-web-module-imports",
|
||||
"trace-web-source-location",
|
||||
"uninstall",
|
||||
"update",
|
||||
"verify-managed-native-boundaries",
|
||||
@@ -656,15 +664,15 @@
|
||||
},
|
||||
"runtime_catalog": {
|
||||
"counts": {
|
||||
"cli_commands": 84,
|
||||
"mcp_tools": 129,
|
||||
"cli_commands": 86,
|
||||
"mcp_tools": 131,
|
||||
"mcp_prompts": 6
|
||||
},
|
||||
"digests": {
|
||||
"tools_sha256": "53ac19960ec7b9907a63951a4e4d8eff02a6c41ad04a4611d61370b8511cc783",
|
||||
"tools_sha256": "f49e05c52c6fd9955edcede27fbaaa248fdb917caa7b27edb8d97f7a30ad250e",
|
||||
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
|
||||
"combined_sha256": "856ce0f99cc6e858f3317e40cec27c2cfd479ae7ff6a8d9cf695504d6166b64d",
|
||||
"providers_sha256": "6e1de7ee30b9b1ff633abfa4fd2b3848bb58e6e76a2ed196aa01836a202e1941"
|
||||
"combined_sha256": "54ac160db3b96580a9a0203a07fe3db01ffc05224db621f48b4abdf979e4f50b",
|
||||
"providers_sha256": "a7d885531ee25d42c51385444faec544d34cd7eba6250a7dc75c4faedd4454a3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+125
-13
@@ -32,6 +32,12 @@ registries but do not cross production filesystem, process, socket, or browser b
|
||||
Boundary tests cross one production boundary. Only acceptance tests assemble
|
||||
the complete runtime or invoke the compiled product surface.
|
||||
|
||||
Binary session, registry, lazy-client and snapshot module cases live under
|
||||
`src/application/binary/`. Their composition cases stay in
|
||||
`tests/composition/analysis-sessions/`; snapshot persistence and actual SDK calls
|
||||
stay in the filesystem and MCP boundary lanes. The shared injected session fixture
|
||||
serves several capabilities and remains under `tests/fixtures/`.
|
||||
|
||||
Focused immutable builders and recording ports shared by one test family live
|
||||
beside their production owner as `src/**/*.fixture.ts`. They are typechecked
|
||||
with the suite and excluded from package builds; broader runtime and provider
|
||||
@@ -55,6 +61,16 @@ needed, exercise the actual target through browser, Electron, or process
|
||||
capture. Real model trials are manual; Vitest covers deterministic evaluator
|
||||
logic.
|
||||
|
||||
`verify:managed` runs the portable PE byte-fixture conformance entrypoint under
|
||||
`scripts/verify/managed/`, with its byte builder under
|
||||
`scripts/fixtures/managed/`. It checks static classification, members,
|
||||
reconstruction, native-boundary relationships and application graphs without
|
||||
executing fixture PE files. Operator-local manifests and actual ILSpy oracles
|
||||
remain optional, separately reported checks; the real Ghidra NativeAOT lane has
|
||||
its own toolchain prerequisites. See [the managed guide](managed-code-analysis.md)
|
||||
for those configurations. Generated completion-ledger checks use the same owning
|
||||
entrypoint and include its verifier/fixture files in their cache inputs.
|
||||
|
||||
## End-to-end, integration and golden evidence
|
||||
|
||||
Full E2E tests invoke the production command dispatcher and real providers,
|
||||
@@ -65,6 +81,13 @@ an XML graph golden. `verify:asset-catalog` compiles source-owned colors with
|
||||
`actool`, invokes real `assetutil`, then checks CLI/MCP results, exact catalog
|
||||
digest, every raw metadata field, pagination and malformed input rejection.
|
||||
Neither artifact workflow requires Hopper or launches it. Both run in macOS CI.
|
||||
`verify:macos-bundle` needs only macOS with Command Line Tools. It compiles a
|
||||
source-owned app with `clang`: a versioned framework, XPC services, an app
|
||||
extension, a login item, a privileged helper, launchd plists, and a helper tool,
|
||||
signed ad hoc. It packs the app as a directory, a `ditto` ZIP, and an APFS DMG,
|
||||
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.
|
||||
|
||||
MCP SDK transport tests with recording providers remain integration tests.
|
||||
They are useful for schema drift and failure projection but do not prove that
|
||||
@@ -106,6 +129,14 @@ It uses an installed browser and accepts an optional installed REA entrypoint.
|
||||
The complete `verify:browser` lane also exercises passive script export through
|
||||
both public adapters. See [website script export](website-script-export.md).
|
||||
|
||||
`verify:browser:modules` compares CLI and stdio MCP traces against an independent
|
||||
real Chromium module-loading fixture: import-map scopes, package prefixes,
|
||||
null/backtracking rejection, query/fragment identities, repeated module instances,
|
||||
lazy and computed unknowns, exact source/map identities and no implicit refetch.
|
||||
Set `REA_BROWSER_EXECUTABLE`; the optional script argument selects an installed
|
||||
package entrypoint. The complete `verify:browser` lane and existing conditional
|
||||
Chrome CI include this verifier. See [module relationships](website-module-trace.md).
|
||||
|
||||
## Real-toolchain verification lanes
|
||||
|
||||
Each real-toolchain command must require only the host tools needed to prove
|
||||
@@ -165,12 +196,21 @@ requires unresolved table bounds or case mappings to remain visible as
|
||||
residual unknowns.
|
||||
|
||||
`npm run verify:inspector` requires the supported Node.js runtime and installed
|
||||
REA dependencies. CI runs it on Linux and Windows. It starts owned loopback
|
||||
REA dependencies. CI runs it on Linux and macOS x64/arm64 and Windows x64. It starts owned loopback
|
||||
Node Inspector fixtures and verifies discovery and passive observation through
|
||||
the CLI and stdio MCP, including special filenames, unresolved discovery
|
||||
locations, and independently resolved loaded scripts. Double-quote filenames
|
||||
are tested on POSIX only because Windows does not support them.
|
||||
|
||||
Inspector adapters live in `src/inspector/`, with loopback fixtures under
|
||||
`tests/fixtures/inspector/` and forked producer boundaries under
|
||||
`tests/boundary/inspector/`. The real lane is owned by
|
||||
`scripts/verify/inspector/runtime-observation.mjs` and
|
||||
`tests/conformance/inspector/`. Browser and Inspector deliberately share the
|
||||
existing browser CDP transport/value and file-location helpers. Node fixture
|
||||
success proves this Inspector workflow on the tested host; it does not prove
|
||||
Electron GUI behavior or another engine.
|
||||
|
||||
## Android APK analysis
|
||||
|
||||
`npm run verify:android` requires an existing Java 17+ and an explicit
|
||||
@@ -181,9 +221,17 @@ kept under ignored `_reference/`. No Gradle build, Android SDK, emulator or
|
||||
application execution is required. The lane compares real CLI/MCP package,
|
||||
class search, class inventory, method decompilation and incoming references.
|
||||
See [Android analysis](android-analysis.md) for boundaries and resource budgets.
|
||||
Authenticated IPA inventory projection is documented in
|
||||
Authenticated IPA and macOS application inventory projection is documented in
|
||||
[Apple application analysis](apple-application-analysis.md).
|
||||
|
||||
The lane and owned-process cancellation helper live in `scripts/verify/android/`.
|
||||
The explicit downloader and fixed manifest live in `scripts/fixtures/android/`;
|
||||
their default remains repository-root `_reference/apk-integration/`. Producer
|
||||
fixtures live in `tests/fixtures/android/`, boundary cases in
|
||||
`tests/boundary/android/`, and inventory projection composition cases in
|
||||
`tests/composition/android/`. Real engine success and synthetic protocol success
|
||||
are separate proof levels.
|
||||
|
||||
Synthetic producer regressions run independently:
|
||||
|
||||
```sh
|
||||
@@ -281,6 +329,35 @@ Keep the provider-specific acceptance path independent from optional
|
||||
cross-compilers. Cross-format failures belong to the cross-format lane and must
|
||||
not make native host acceptance unavailable.
|
||||
|
||||
## Native platform baseline in CI
|
||||
|
||||
CI exercises the pinned Node.js runtime on native hosted runners:
|
||||
|
||||
| Host | Runner | Baseline checks |
|
||||
| --------------------- | ------------------ | ---------------------------------------------------------------------------------------- |
|
||||
| Linux x64 | `ubuntu-latest` | Installed package and real Node Inspector CLI/MCP |
|
||||
| Linux arm64 (aarch64) | `ubuntu-24.04-arm` | Installed package and real Node Inspector CLI/MCP |
|
||||
| macOS 15 arm64 | `macos-15` | Installed package and real Node Inspector CLI/MCP |
|
||||
| macOS 15 x64 | `macos-15-intel` | Installed package and real Node Inspector CLI/MCP |
|
||||
| Windows x64 | `windows-latest` | Curated capabilities, native controls, installed package and real Node Inspector CLI/MCP |
|
||||
|
||||
Package and Inspector matrices assert the actual Node platform/architecture
|
||||
before verification and record those values with the Node version. Each matrix
|
||||
runs at most two jobs concurrently with explicit timeouts and Node heap/thread
|
||||
limits. Package checks cover installation, CLI/MCP discovery, target-free
|
||||
analysis, configuration backups/recovery, Evidence and owned lifecycle; Inspector
|
||||
checks execute source-owned loopback targets and special filename cases.
|
||||
|
||||
macOS uses one OS version with one native baseline job per architecture. Each job
|
||||
runs package and Inspector checks after a single dependency installation. The
|
||||
separate Inspector matrix covers Linux and Windows; Apple artifact checks retain
|
||||
their own macOS 15 arm64 job for Xcode-dependent workflows.
|
||||
|
||||
These native baseline checks complement the Linux source-test shards and the
|
||||
separate Apple-artifact and real-provider lanes. Actual Hopper, Ghidra, IDA,
|
||||
browser and managed-tool claims require their corresponding verification lanes.
|
||||
Runner labels follow the [GitHub hosted-runner reference](https://docs.github.com/en/actions/reference/runners/github-hosted-runners).
|
||||
|
||||
## Developer commands
|
||||
|
||||
Use source feedback while editing, explicit boundary checks for the changed
|
||||
@@ -386,17 +463,24 @@ that explicit lane, not the routine iteration requirement.
|
||||
|
||||
## Apple native metadata and UI
|
||||
|
||||
`npm run verify:apple-dispatch` compiles Objective-C class/protocol and Swift
|
||||
conformance/vtable fixtures, inspects their bytes and repeats after stripping
|
||||
local symbols. It requires macOS and the host Xcode toolchain; targets are not
|
||||
executed. `npm run verify:native-ui` launches exactly one source-owned fixture
|
||||
window and requires successful selected-window capture and selected actions.
|
||||
An OS permission denial fails the positive lane. `npm run verify:native-ui:permissions`
|
||||
allows a host-permission-boundary-only result and explicitly reports
|
||||
`positive_e2e: false`; it must not be reported as capture/action proof.
|
||||
Both commands reject a changed executable digest and clean up the fixture
|
||||
process and helper. These lanes require an interactive macOS desktop. See [native investigation](native-investigation.md)
|
||||
for the exact ABI, authority, graph and observation boundaries.
|
||||
`npm run verify:apple-dispatch` compiles the Objective-C fixture (classes,
|
||||
protocols, a property and an `NSString` category) and the Swift
|
||||
conformance/vtable fixture.
|
||||
|
||||
- Each fixture is linked with legacy `LC_DYLD_INFO` binds and with chained
|
||||
fixups; on Apple silicon the ObjC fixture is also built as arm64e, which uses
|
||||
authenticated pointers.
|
||||
- The lane inspects the bytes and repeats after stripping local symbols.
|
||||
- It requires the bound `NSObject` superclass, the external category, and the
|
||||
matching `pointer_fixups` coverage. It requires macOS and the host Xcode toolchain; targets are not
|
||||
executed. `npm run verify:native-ui` launches exactly one source-owned fixture
|
||||
window and requires successful selected-window capture and selected actions.
|
||||
An OS permission denial fails the positive lane. `npm run verify:native-ui:permissions`
|
||||
allows a host-permission-boundary-only result and explicitly reports
|
||||
`positive_e2e: false`; it must not be reported as capture/action proof.
|
||||
Both commands reject a changed executable digest and clean up the fixture
|
||||
process and helper. These lanes require an interactive macOS desktop. See [native investigation](native-investigation.md)
|
||||
for the exact ABI, authority, graph and observation boundaries.
|
||||
|
||||
### Firmware adapters
|
||||
|
||||
@@ -412,6 +496,12 @@ unknown chunks, depth limits and extracted child digests. The optional
|
||||
Neither optional toolchain is a base-lane prerequisite. See
|
||||
[firmware analysis](firmware-analysis.md) for limits and unverified formats.
|
||||
|
||||
The real entrypoint is `scripts/verify/firmware/analysis.mjs`; the fixture runner
|
||||
and unchanged Python producer belong together in `scripts/fixtures/firmware/`.
|
||||
Producer fixtures and boundaries remain in `tests/fixtures/firmware/` and
|
||||
`tests/boundary/firmware/`. Source-fixture generation proves the runner and
|
||||
independent oracle; actual Binwalk/Unblob CLI/MCP proof requires selected tools.
|
||||
|
||||
### JavaScript source recovery
|
||||
|
||||
Build once with `npm run build:cached`, then run `npm run verify:javascript:recovery`.
|
||||
@@ -425,3 +515,25 @@ and compares a finite set of known fixture results. It does not establish
|
||||
arbitrary recovered-application equivalence. CI installs these prerequisites only
|
||||
in `.github/workflows/real-javascript-recovery.yml`; the existing `real-browser`
|
||||
lane uses real Chrome for browser capture and website workflows.
|
||||
|
||||
### Captured website source-map lane
|
||||
|
||||
`npm run verify:browser:source-maps` checks actual Chromium capture/export and
|
||||
source-map point tracing through CLI and stdio MCP. It requires absolute
|
||||
`REA_BROWSER_EXECUTABLE` and `REA_WEB_SOURCE_MAP_COMPILER` pointing to esbuild
|
||||
0.25.10's `lib/main.js` in a caller-owned isolated installation. Preflight reports
|
||||
missing prerequisites for this lane. The compiler is used only to generate the
|
||||
source-owned fixture. No Hopper, Ghidra or application dependency installation
|
||||
is required.
|
||||
|
||||
```bash
|
||||
REA_BROWSER_EXECUTABLE=/absolute/path/to/chromium \
|
||||
REA_WEB_SOURCE_MAP_COMPILER=/absolute/path/to/fixture-tools/node_modules/esbuild/lib/main.js \
|
||||
npm run verify:browser:source-maps
|
||||
```
|
||||
|
||||
`scripts/verify-browser-source-maps.mjs /absolute/path/to/installed/rea.mjs`
|
||||
checks an installed package after building the verifier dependencies. The separate
|
||||
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.
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
{
|
||||
"manifest_id": "ecm_ae079950d6d61294a0940a34379f6a3c1cb26e249557ca62ba72aa1e755886e1",
|
||||
"manifest_id": "ecm_e7e60d5d08bfe335c4c0b38ab4598eb634444b835202e325241749d152808c94",
|
||||
"verifier": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
@@ -13,7 +13,7 @@
|
||||
"skill_digests": [
|
||||
{
|
||||
"skill_id": "reverse-engineer-anything",
|
||||
"sha256": "ee03e19730d1dde6ab46b767120b090074f59166868cfd9f547e2b6d217627b7"
|
||||
"sha256": "265556bd52f52ad829187fe71831cf05329248286c86e3102f0cdbf8841e1d6d"
|
||||
}
|
||||
],
|
||||
"claims": [
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# Captured website source locations
|
||||
|
||||
`trace_web_source_location` maps one selected position in a script retained by
|
||||
`export_web_scripts` through one explicitly paired local source map. The shared
|
||||
CLI/MCP workflow verifies actual manifest, script and map byte identities and
|
||||
returns the original position and embedded source inline. It acquires no browser,
|
||||
fetches no URL and executes no application code.
|
||||
|
||||
```bash
|
||||
rea trace-web-source-location \
|
||||
/analysis/scripts/manifest.json 0 \
|
||||
/analysis/app.js.map 'https://example.test/maps/app.js.map?v=7#context' \
|
||||
1 25 --json
|
||||
```
|
||||
|
||||
The six positional arguments select the manifest, zero-based script index,
|
||||
absolute map path, absolute map URL context, one-based generated line and
|
||||
zero-based UTF-16 column. The MCP arguments are:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_path": "/analysis/scripts/manifest.json",
|
||||
"script_index": 0,
|
||||
"source_map": {
|
||||
"path": "/analysis/app.js.map",
|
||||
"url": "https://example.test/maps/app.js.map?v=7#context"
|
||||
},
|
||||
"generated_position": { "line": 1, "column": 25 }
|
||||
}
|
||||
```
|
||||
|
||||
The URL supplies the context for resolving `sources` and `sourceRoot`; it is not
|
||||
an instruction to fetch. Absolute virtual or file contexts can be selected as
|
||||
well. The map/script association is caller-selected: a declaration or local map
|
||||
does not prove deployment authenticity or execution. Use
|
||||
[import tracing](website-module-trace.md) to inspect outgoing native module edges.
|
||||
|
||||
## Meaning of the evidence
|
||||
|
||||
Lookup follows the greatest generated position less than or equal to the
|
||||
selected point, across lines, and returns **every** segment at that position.
|
||||
It includes generated-only mappings. This follows
|
||||
[ECMA-426 GetOriginalPositions](https://tc39.es/ecma426/#sec-getoriginalpositions),
|
||||
with one-based lines in REA's public contract. A point result does not establish
|
||||
coverage of a source range, execution or UI causality.
|
||||
|
||||
Mapped entries preserve the raw source name/root, section path, section-local
|
||||
source index and flattened index. Duplicate URLs remain distinct, including
|
||||
entries with different embedded contents. Missing source names retain a null
|
||||
resolved URL. Original coordinates outside embedded text retain the reported
|
||||
coordinates and a null offset.
|
||||
|
||||
Embedded content hashes identify the UTF-8 encoding of the decoded
|
||||
`sourcesContent` string, including its line endings. They do not identify an
|
||||
independently captured original file. Raw map text is retained in Evidence's
|
||||
`raw_result`, including unknown extensions; these extensions are not interpreted.
|
||||
Capture context and source Evidence links accompany independently verified
|
||||
selected file identities.
|
||||
|
||||
## Adapter and resource boundaries
|
||||
|
||||
The adapter uses the locked upstream
|
||||
[`@jridgewell/trace-mapping` 0.3.31](https://github.com/jridgewell/trace-mapping)
|
||||
without modifying its code. It validates map/VLQ structure before allocation,
|
||||
normalizes nested indexed offsets before upstream flattening, checks section
|
||||
boundaries and indexes, and retains raw declaration identity. It supports regular
|
||||
maps and inline indexed sections. External section URLs return an actionable
|
||||
unsupported-profile error. This is a defined decoder profile, not a claim to
|
||||
implement every current source-map extension.
|
||||
|
||||
One owned Node subprocess decodes the map. Its private request directory is
|
||||
removed before success is returned. Only this child's inherited `NODE_OPTIONS`
|
||||
is removed; explicit V8 flags set a 192 MiB old-generation and 8 MiB semi-space
|
||||
budget, one V8 worker and a 20-second deadline. Reported actual V8 heap limits
|
||||
are checked. These are heap limits, not an aggregate process RSS limit. Cleanup
|
||||
may extend the deadline and an unconfirmed cleanup overrides success with
|
||||
`cleanup_incomplete` and the owning operation/resources.
|
||||
|
||||
Complete evidence is bounded by a 4 MiB map, 32 MiB codec reply, 262144 decoded
|
||||
rows/segments and 64 nested section levels. Exceeding a budget returns an explicit
|
||||
failure, without partial results. The existing 32 MiB manifest and 16 MiB selected
|
||||
script boundaries also apply. No global toolchain or browser configuration changes
|
||||
are made.
|
||||
|
||||
## Verification
|
||||
|
||||
Focused tests cover duplicate mappings, indexed offsets/overlap/local indexes,
|
||||
32-bit VLQ overflow, nullable content, UTF-16/CRLF positions, immutable artifact
|
||||
identity, cancellation, late output closure, cleanup failures, SDK schemas and
|
||||
public CLI behavior. The actual codec process is exercised on the test host.
|
||||
|
||||
The optional `verify:browser:source-maps` lane compiles a source-owned TypeScript
|
||||
fixture with isolated esbuild 0.25.10, serves it to real Chromium, captures and
|
||||
exports the actual response, then checks the original compiler point and exact
|
||||
retained content through CLI and stdio MCP. It can target an installed package.
|
||||
Only this lane requires Chromium and the fixture compiler. Linux x64 is the real
|
||||
workflow covered by the dedicated CI job; other host workflows remain unverified.
|
||||
@@ -0,0 +1,85 @@
|
||||
# Captured website module relationships
|
||||
|
||||
`trace_web_module_imports` / `rea trace-web-module-imports` traces one script's
|
||||
outgoing native ES imports and re-exports from an
|
||||
[exported capture](website-script-export.md). It verifies the selected source's
|
||||
SHA-256 and byte count, preserves browser URL identities, and returns every
|
||||
matching capture candidate inline. It does not refetch website assets.
|
||||
|
||||
## Select the source and context
|
||||
|
||||
Provide a caller-supplied Chromium executable supporting `import.meta.resolve`.
|
||||
REA does not install or upgrade a browser. Real verification covers Linux x64;
|
||||
other hosts have not received this feature's real workflow verification.
|
||||
|
||||
```bash
|
||||
REA_BROWSER_EXECUTABLE=/absolute/path/to/chrome \
|
||||
rea trace-web-module-imports /analysis/export/manifest.json 0 \
|
||||
--importer-url 'https://example.test/app/main.js?build=2#entry' \
|
||||
--import-map-path /analysis/import-map.json \
|
||||
--import-map-base-url https://example.test/maps/ \
|
||||
--json
|
||||
```
|
||||
|
||||
The MCP input expresses the same choices:
|
||||
|
||||
```json
|
||||
{
|
||||
"manifest_path": "/analysis/export/manifest.json",
|
||||
"script_index": 0,
|
||||
"importer_url": "https://example.test/app/main.js?build=2#entry",
|
||||
"import_map": {
|
||||
"path": "/analysis/import-map.json",
|
||||
"base_url": "https://example.test/maps/"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`script_index` is zero-based in `manifest.scripts`. If `importer_url` is omitted,
|
||||
the trace selects the reported source URL. An inline script's document base
|
||||
cannot be established from that URL alone; choose the context explicitly when
|
||||
known. `import_map` is optional and reads local JSON only. Its exact bytes and
|
||||
base URL are retained; selecting it does not prove the map was installed in the
|
||||
captured page. HTTP(S) importer contexts without URL userinfo are supported initially.
|
||||
|
||||
## Interpret the evidence
|
||||
|
||||
- Static imports, re-exports and literal dynamic imports receive the native
|
||||
engine's resolved URL or exact rejection message. Empty strings are retained.
|
||||
- Computed dynamic imports retain their expression and source positions with
|
||||
`computed-specifier` unknown resolution. Erased type-only imports/exports and
|
||||
inferred bundler IDs are excluded from native import claims.
|
||||
- Locations use UTF-16 offsets and columns, with one-based lines. Parse recovery
|
||||
diagnostics and unparseable input are explicit.
|
||||
- Query variants and fragments remain distinct. An exact reported URL is an
|
||||
`exact-reported-url` candidate. A response URL stripped of its fragment is a
|
||||
weaker `response-url-without-fragment` content candidate. Multiple captured
|
||||
versions are all returned; the trace selects none as canonical.
|
||||
- Candidate metadata remains reported capture evidence; only the selected
|
||||
source's bytes are reverified. Missing/lazy targets can resolve successfully
|
||||
with no captured candidate. Every relationship reports execution as unknown.
|
||||
- Source-map declarations remain declarations; this operation does not read or
|
||||
invent map bytes, original-source mappings, worker ownership or UI causality.
|
||||
|
||||
The selected source is loaded from `files/` under the manifest's current
|
||||
directory. Relocation retains the manifest's originally reported paths and the
|
||||
actual current source path separately. Unsafe paths, symlink source directories,
|
||||
nonregular files, wrong digests/sizes and malformed UTF-8 fail explicitly.
|
||||
|
||||
## Engine and lifecycle
|
||||
|
||||
An owned temporary browser context runs REA's trusted resolver stub. The selected
|
||||
application is parsed as data and is never executed by this trace. Page requests
|
||||
are fulfilled with REA's synthetic document/stub or blocked; no website asset is
|
||||
downloaded. This does not establish an OS sandbox or browser-wide network
|
||||
containment. The engine version and native report are retained in Evidence.
|
||||
|
||||
Sources without literal imports do not acquire a browser. The manifest, source
|
||||
and import map have 32 MiB, 16 MiB and 4 MiB input limits. Native resolution has
|
||||
one 20-second deadline; cleanup may extend that deadline. One renderer is
|
||||
requested, and there is no aggregate host memory-limit claim. Cancellation
|
||||
closes only the owned browser; cleanup failure cannot return a successful trace.
|
||||
`REA_BROWSER_NO_SANDBOX=true` passes Chromium's corresponding launch option when
|
||||
the host requires it; it does not enable an REA security boundary.
|
||||
|
||||
See [testing](testing.md) for the native loading oracle and CLI/MCP real lane.
|
||||
@@ -148,7 +148,7 @@ private runtime files; crash-time transactional deletion is not claimed.
|
||||
|
||||
## Verification
|
||||
|
||||
See [native controls](../native/windows/README.md) for the separate artifact
|
||||
See [native controls](https://github.com/morluto/rea/blob/main/native/windows/README.md) for the separate artifact
|
||||
build lane and native boundary checks. Development tarballs without the built
|
||||
artifact report native controls unavailable; publishing requires the verified
|
||||
artifact and manifest.
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
"$schema": "./node_modules/knip/schema.json",
|
||||
"entry": ["src/main.ts", "src/cli.ts", "src/contracts/errorSchemas.ts"],
|
||||
"project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"],
|
||||
"vitepress": {
|
||||
"entry": ["docs/.vitepress/config.ts"]
|
||||
},
|
||||
"ignoreBinaries": ["mkfifo"],
|
||||
"ignoreDependencies": ["electron", "esbuild", "webpack"],
|
||||
"ignoreExportsUsedInFile": true,
|
||||
"rules": {
|
||||
|
||||
Generated
+2575
-5
File diff suppressed because it is too large
Load Diff
+25
-11
@@ -42,10 +42,12 @@
|
||||
"dist",
|
||||
"install.sh",
|
||||
"bridge/hopper_bridge.py",
|
||||
"bridge/native/NativeUIChildren.swift",
|
||||
"bridge/native/ReaNativeUI.swift",
|
||||
"bridge/ghidra/ReaGhidraBridge.java",
|
||||
"bridge/ghidra/ReaGhidraExtensions.java",
|
||||
"bridge/ghidra/ReaGhidraPrepareCom.java",
|
||||
"bridge/android/ReaJadxBridge.java",
|
||||
"scripts/rea.mjs",
|
||||
"scripts/electron-active-hook.cjs",
|
||||
"scripts/electron-active-hook-boundaries.cjs",
|
||||
@@ -122,6 +124,7 @@
|
||||
"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: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",
|
||||
@@ -135,15 +138,15 @@
|
||||
"verify:browser:dom": "node scripts/verify-browser-dom-destinations.mjs",
|
||||
"verify:browser:network": "npm run build:cached && node scripts/verify-browser-network.mjs",
|
||||
"verify:electron": "npm run build:cached && node scripts/verify-real-electron.mjs",
|
||||
"verify:inspector": "npm run build:cached && node scripts/verify-real-javascript-runtime-observation.mjs",
|
||||
"verify:javascript-runtime": "npm run build:cached && node scripts/verify-local-javascript-runtime.mjs",
|
||||
"verify:javascript:digests": "npm run build:cached && node scripts/verify-large-javascript-digests.mjs",
|
||||
"verify:application-workflows": "npm run build:cached && node scripts/verify-local-application-workflows.mjs",
|
||||
"verify:managed": "npm run build:cached && node scripts/verify-managed-conformance.mjs",
|
||||
"verify:firmware": "npm run build:cached && node scripts/verify-real-firmware.mjs",
|
||||
"fixtures:firmware": "node scripts/generate-firmware-fixtures.mjs",
|
||||
"verify:android": "npm run build:cached && node scripts/verify-real-android.mjs",
|
||||
"fixtures:android": "node scripts/fetch-android-fixtures.mjs",
|
||||
"verify:inspector": "npm run build:cached && node scripts/verify/inspector/runtime-observation.mjs",
|
||||
"verify:javascript-runtime": "npm run build:cached && node scripts/verify/javascript/runtime-observation.mjs",
|
||||
"verify:javascript:digests": "npm run build:cached && node scripts/verify/javascript/digests.mjs",
|
||||
"verify:application-workflows": "npm run build:cached && node scripts/verify/javascript/application-workflows.mjs",
|
||||
"verify:managed": "npm run build:cached && node scripts/verify/managed/conformance.mjs",
|
||||
"verify:firmware": "npm run build:cached && node scripts/verify/firmware/analysis.mjs",
|
||||
"fixtures:firmware": "node scripts/fixtures/firmware/generate.mjs",
|
||||
"verify:android": "npm run build:cached && node scripts/verify/android/analysis.mjs",
|
||||
"fixtures:android": "node scripts/fixtures/android/fetch.mjs",
|
||||
"verify:agent": "npm run build:cached && node scripts/verify-agent-experience.mjs",
|
||||
"verify:hopper:linux": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper-linux.mjs",
|
||||
"verify:setup:linux": "npm run build:cached && node scripts/verify-linux-setup-e2e.mjs",
|
||||
@@ -158,6 +161,9 @@
|
||||
"config:print": "node scripts/print-mcp-config.mjs",
|
||||
"evidence:generate": "npm run build:cached && node scripts/generate-completion-ledger.mjs",
|
||||
"evidence:check": "npm run build:cached && node scripts/generate-completion-ledger.mjs --check",
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs",
|
||||
"docs:generate": "node scripts/run-exclusive.mjs docs npm run docs:generate:unlocked",
|
||||
"docs:generate:unlocked": "npm run metadata:generate && npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && node scripts/generate-completion-ledger.mjs && node scripts/generate-error-schema.mjs",
|
||||
"docs:check": "node scripts/run-exclusive.mjs docs turbo run docs:check:built",
|
||||
@@ -177,9 +183,11 @@
|
||||
"build:fixtures:nativeaot": "node scripts/generate-nativeaot-fixtures.mjs",
|
||||
"verify:ghidra:nativeaot": "npm run build:cached && node scripts/verify-real-ghidra-nativeaot.mjs",
|
||||
"verify:browser:scripts": "npm run build:cached && node scripts/verify-browser-scripts.mjs",
|
||||
"verify:browser:modules": "npm run build:cached && node scripts/verify-browser-modules.mjs",
|
||||
"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: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"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
|
||||
@@ -216,6 +224,11 @@
|
||||
"ws": "8.21.0",
|
||||
"zod": "4.4.3"
|
||||
},
|
||||
"overrides": {
|
||||
"vitepress": {
|
||||
"vite": "6.4.4"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fast-check/vitest": "0.5.0",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
@@ -233,6 +246,7 @@
|
||||
"oxlint": "1.87.0",
|
||||
"turbo": "2.11.7",
|
||||
"typescript": "7.0.2",
|
||||
"vitepress": "1.6.4",
|
||||
"vitest": "5.0.3",
|
||||
"yaml": "2.9.0"
|
||||
}
|
||||
|
||||
@@ -3,15 +3,17 @@ import { createReadStream } from "node:fs";
|
||||
import { link, mkdir, open, rm } from "node:fs/promises";
|
||||
import { resolve, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { JADX_RELEASE } from "../src/android/JadxRelease.ts";
|
||||
import fixture from "./fixtures/android-apidemos.json" with { type: "json" };
|
||||
import { JADX_RELEASE } from "../../../src/android/JadxRelease.ts";
|
||||
import fixture from "./apidemos.json" with { type: "json" };
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length > 1)
|
||||
throw new Error("Usage: node scripts/fetch-android-fixtures.mjs [DIRECTORY]");
|
||||
throw new Error("Usage: npm run fixtures:android -- [DIRECTORY]");
|
||||
const directory = resolve(
|
||||
args[0] ??
|
||||
fileURLToPath(new URL("../_reference/apk-integration/", import.meta.url)),
|
||||
fileURLToPath(
|
||||
new URL("../../../_reference/apk-integration/", import.meta.url),
|
||||
),
|
||||
);
|
||||
await mkdir(directory, { recursive: true });
|
||||
const hashFile = async (path) => {
|
||||
@@ -0,0 +1,61 @@
|
||||
import { createServer } from "node:http";
|
||||
|
||||
/** Source-owned module graph with actual native loading and an uncalled lazy edge. */
|
||||
export async function startBrowserModuleSite() {
|
||||
const requests = [];
|
||||
const importMap = {
|
||||
imports: { "pkg/": "/pkg/", alias: "./global.js", blocked: null },
|
||||
scopes: { "/scoped/": { alias: "./scoped.js" } },
|
||||
};
|
||||
const main = `
|
||||
import {marker as alias} from 'alias';
|
||||
import {marker as pkg} from 'pkg/nested.js';
|
||||
const a = await import('/query.js?v=1#one');
|
||||
const b = await import('/query.js?v=1#two');
|
||||
const c = await import('/query.js?v=2');
|
||||
const again = await import('/query.js?v=1#one');
|
||||
const specifiers = ['alias', 'pkg/nested.js', '/query.js?v=1#one', '/query.js?v=1#two', '/query.js?v=2', '/query.js?v=1#one', './lazy.js', 'blocked', 'pkg/../escape.js', 'pkg/%2e%2e/escape.js', ''];
|
||||
globalThis.__moduleProof = {alias, pkg, urls: [a.url,b.url,c.url], instances: [a.instance,b.instance,c.instance], same_module: again === a, different_fragments: a !== b, resolutions: specifiers.map(specifier => {try{return {state: 'resolved', url: import.meta.resolve(specifier)}}catch(error){return {state: 'rejected', message: String(error)}}})};
|
||||
document.body.dataset.ready = 'true';
|
||||
function uncalled(){return import('./lazy.js')}
|
||||
function blocked(){return import('blocked')}
|
||||
function escaped(){return import('pkg/../escape.js')}
|
||||
function encodedEscape(){return import('pkg/%2e%2e/escape.js')}
|
||||
function empty(){return import('')}
|
||||
function computed(name){return import(name)}
|
||||
`;
|
||||
const server = createServer((request, response) => {
|
||||
requests.push(request.url);
|
||||
if (request.url === "/")
|
||||
return response
|
||||
.writeHead(200, { "Content-Type": "text/html" })
|
||||
.end(
|
||||
`<body><base href="/maps/"><script type="importmap">${JSON.stringify(importMap)}</script><script type="module" src="/scoped/main.js?build=2#entry"></script></body>`,
|
||||
);
|
||||
response.setHeader("Content-Type", "text/javascript");
|
||||
if (request.url?.startsWith("/scoped/main.js?")) return response.end(main);
|
||||
if (request.url === "/maps/scoped.js")
|
||||
return response.end("export const marker = 'scoped';");
|
||||
if (request.url === "/pkg/nested.js")
|
||||
return response.end("export const marker = 'pkg';");
|
||||
if (request.url?.startsWith("/query.js?"))
|
||||
return response.end(
|
||||
"globalThis.__moduleCount = (globalThis.__moduleCount ?? 0) + 1; export const instance = globalThis.__moduleCount; export const url = import.meta.url;",
|
||||
);
|
||||
response.writeHead(404).end("Not selected");
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address();
|
||||
if (address === null || typeof address === "string")
|
||||
throw new Error("Module fixture has no TCP address");
|
||||
return {
|
||||
origin: `http://127.0.0.1:${address.port}`,
|
||||
requests,
|
||||
importMap,
|
||||
main,
|
||||
close: () =>
|
||||
new Promise((resolve, reject) =>
|
||||
server.close((error) => (error ? reject(error) : resolve())),
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { createServer } from "node:http";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { access } from "node:fs/promises";
|
||||
import { isAbsolute } from "node:path";
|
||||
|
||||
/** Generate a real compiler map and serve only source-owned fixture code. */
|
||||
export async function startBrowserSourceMapSite() {
|
||||
const command = process.env.REA_WEB_SOURCE_MAP_COMPILER;
|
||||
if (!command || !isAbsolute(command))
|
||||
throw new Error(
|
||||
"verify:browser:source-maps requires absolute REA_WEB_SOURCE_MAP_COMPILER pointing to isolated esbuild 0.25.10 lib/main.js",
|
||||
);
|
||||
await access(command);
|
||||
const compiler = await import(pathToFileURL(command).href);
|
||||
if (compiler.version !== "0.25.10")
|
||||
throw new Error(`Expected esbuild 0.25.10; observed ${compiler.version}`);
|
||||
const original =
|
||||
'// 🎋 UTF-16 and CRLF fixture\r\nfunction boot(): void {\r\n const marker: string = "source-map proof";\r\n document.body.dataset.ready = marker;\r\n}\r\nboot();\r\n';
|
||||
const compiled = await compiler
|
||||
.transform(original, {
|
||||
loader: "ts",
|
||||
sourcefile: "../src/fixture.ts?v=7#original",
|
||||
sourcemap: "external",
|
||||
minify: true,
|
||||
format: "iife",
|
||||
target: "es2022",
|
||||
})
|
||||
.finally(() => compiler.stop());
|
||||
const token = '"source-map proof"';
|
||||
const generatedOffset = compiled.code.indexOf(token);
|
||||
const originalOffset = original.indexOf(token);
|
||||
if (generatedOffset < 0 || originalOffset < 0)
|
||||
throw new Error("Compiler fixture marker missing");
|
||||
const position = textPosition(compiled.code, generatedOffset);
|
||||
const originalPosition = textPosition(original, originalOffset);
|
||||
let mapRequests = 0;
|
||||
const server = createServer((request, response) => {
|
||||
if (request.url === "/")
|
||||
return response
|
||||
.writeHead(200, { "Content-Type": "text/html" })
|
||||
.end('<body><script src="/app.js?build=7"></script></body>');
|
||||
if (request.url === "/app.js?build=7")
|
||||
return response
|
||||
.writeHead(200, { "Content-Type": "text/javascript" })
|
||||
.end(compiled.code);
|
||||
if (request.url?.startsWith("/maps/app.js.map")) {
|
||||
mapRequests += 1;
|
||||
return response
|
||||
.writeHead(200, { "Content-Type": "application/json" })
|
||||
.end(compiled.map);
|
||||
}
|
||||
response.writeHead(404).end("Unselected");
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string")
|
||||
throw new Error("Source map fixture has no TCP address");
|
||||
return {
|
||||
origin: `http://127.0.0.1:${address.port}`,
|
||||
original,
|
||||
generated: compiled.code,
|
||||
map: compiled.map,
|
||||
position,
|
||||
originalPosition,
|
||||
originalOffset,
|
||||
mapRequests: () => mapRequests,
|
||||
compiler: { id: "esbuild", version: compiler.version },
|
||||
close: () =>
|
||||
new Promise((resolve, reject) =>
|
||||
server.close((error) => (error ? reject(error) : resolve())),
|
||||
),
|
||||
};
|
||||
}
|
||||
const textPosition = (text, offset) => {
|
||||
const preceding = text.slice(0, offset).split(/\r\n?|\n|\u2028|\u2029/u);
|
||||
return { line: preceding.length, column: preceding.at(-1).length };
|
||||
};
|
||||
@@ -34,7 +34,7 @@ const ext4 =
|
||||
: [];
|
||||
for (const command of ext4) await exec(command, ["-V"]);
|
||||
const generated = await exec(process.env.REA_FIRMWARE_PYTHON ?? "python3", [
|
||||
fileURLToPath(new URL("./lib/generate-firmware-fixture.py", import.meta.url)),
|
||||
fileURLToPath(new URL("./generate.py", import.meta.url)),
|
||||
root,
|
||||
process.env.REA_FIRMWARE_CC ?? "cc",
|
||||
...ext4,
|
||||
+2
-6
@@ -17,9 +17,7 @@ export async function buildRecoveryFixtures(root, prefix) {
|
||||
throw new Error(
|
||||
"Fixture compilers must be esbuild 0.25.10 and webpack 5.101.3",
|
||||
);
|
||||
const source = fileURLToPath(
|
||||
new URL("../fixtures/javascript-recovery/entry.mjs", import.meta.url),
|
||||
);
|
||||
const source = fileURLToPath(new URL("./entry.mjs", import.meta.url));
|
||||
const webpackOutput = join(root, "webpack");
|
||||
await mkdir(webpackOutput);
|
||||
await new Promise((resolve, reject) => {
|
||||
@@ -50,9 +48,7 @@ export async function buildRecoveryFixtures(root, prefix) {
|
||||
});
|
||||
// One self-contained source fixture avoids unresolved imports in the oracle.
|
||||
const helperSource = await readFile(
|
||||
fileURLToPath(
|
||||
new URL("../fixtures/javascript-recovery/helpers.mjs", import.meta.url),
|
||||
),
|
||||
fileURLToPath(new URL("./helpers.mjs", import.meta.url)),
|
||||
"utf8",
|
||||
);
|
||||
const plain = await esbuild.transform(
|
||||
@@ -22,7 +22,7 @@ delete verifierEnvironment.REA_ILSPY_CMD_PATH;
|
||||
|
||||
const { stdout } = await exec(
|
||||
process.execPath,
|
||||
[join(root, "scripts/verify-managed-conformance.mjs")],
|
||||
[join(root, "scripts/verify/managed/conformance.mjs")],
|
||||
{
|
||||
cwd: root,
|
||||
env: verifierEnvironment,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { McpServer } from "@modelcontextprotocol/server";
|
||||
|
||||
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
|
||||
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managedWorkflowToolContracts.js";
|
||||
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managed/managedWorkflowToolContracts.js";
|
||||
import { auxiliaryAnalysisProviderDeclarations } from "../dist/composition/auxiliaryAnalysisProviders.js";
|
||||
import { toolRegistrationOptions } from "../dist/server/toolRegistrationOptions.js";
|
||||
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
|
||||
|
||||
@@ -20,11 +20,14 @@ import time
|
||||
# Keep this digest aligned with LinuxHopper.ts and rerun real-Hopper verification
|
||||
# before accepting another vendor build.
|
||||
SUPPORTED_HOPPER_SHA256 = {
|
||||
"0294ced141cc373468ee22d8343e7dac41980cb05a937994ca81c9f09afe7ded"
|
||||
"0294ced141cc373468ee22d8343e7dac41980cb05a937994ca81c9f09afe7ded",
|
||||
"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
|
||||
@@ -196,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]
|
||||
@@ -212,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,
|
||||
@@ -644,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)
|
||||
@@ -659,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:
|
||||
@@ -675,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),
|
||||
)
|
||||
@@ -695,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()
|
||||
|
||||
|
||||
@@ -15,6 +15,12 @@ const environment = () => ({
|
||||
|
||||
/** Execute the compiled CLI with production providers and no test adapters. */
|
||||
export async function artifactCli(command, target, arguments_ = []) {
|
||||
return (await artifactCliEvidence(command, target, arguments_))
|
||||
.normalized_result;
|
||||
}
|
||||
|
||||
/** Execute the compiled CLI and return its complete Evidence record. */
|
||||
export async function artifactCliEvidence(command, target, arguments_ = []) {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[
|
||||
@@ -29,7 +35,7 @@ export async function artifactCli(command, target, arguments_ = []) {
|
||||
const evidence = JSON.parse(stdout);
|
||||
assert.equal(evidence.error, undefined, stdout);
|
||||
assert.ok(evidence.normalized_result, "CLI omitted normalized result");
|
||||
return evidence.normalized_result;
|
||||
return evidence;
|
||||
}
|
||||
|
||||
/** Run a real stdio MCP subprocess, owning and closing its transport. */
|
||||
|
||||
@@ -0,0 +1,390 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { chromium } from "playwright-core";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { parseEvidence } from "../../dist/domain/evidence.js";
|
||||
import { webModuleTraceResultSchema } from "../../dist/domain/webModuleTrace.js";
|
||||
import { NativeModuleResolver } from "../../dist/browser/modules/NativeModuleResolver.js";
|
||||
import { startBrowserModuleSite } from "../fixtures/browser-module-site.mjs";
|
||||
import { mcpTextValue } from "./mcp-verifier-results.mjs";
|
||||
|
||||
/** Check native URL resolution against an independently loaded module graph through CLI/MCP. */
|
||||
export async function verifyBrowserModules(
|
||||
executable,
|
||||
entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url)),
|
||||
) {
|
||||
const site = await startBrowserModuleSite();
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-browser-modules-"));
|
||||
const env = {
|
||||
...process.env,
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_BROWSER_EXECUTABLE: executable,
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
|
||||
};
|
||||
const cli = async (...args) => {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[entrypoint, ...args, "--json"],
|
||||
{ env, timeout: 45000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
return parseEvidence(JSON.parse(stdout));
|
||||
};
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "module-real-e2e", version: "1" });
|
||||
try {
|
||||
const oracle = await loadingOracle(executable, site);
|
||||
const scenario = {
|
||||
browser: { mode: "launch", executable_path: executable },
|
||||
start_url: { url: site.origin },
|
||||
actions: [
|
||||
{
|
||||
step_id: "ready",
|
||||
action: "wait_for",
|
||||
locator: { kind: "css", selector: '[data-ready="true"]' },
|
||||
state: "visible",
|
||||
timeout_ms: 10000,
|
||||
},
|
||||
],
|
||||
capture: { network: { response_body: true } },
|
||||
};
|
||||
const capture = await cli(
|
||||
"capture-browser-scenario",
|
||||
JSON.stringify(scenario),
|
||||
);
|
||||
const capturePath = join(root, "capture.json");
|
||||
await writeFile(capturePath, JSON.stringify(capture));
|
||||
const exported = await cli(
|
||||
"export-web-scripts",
|
||||
capturePath,
|
||||
join(root, "export"),
|
||||
);
|
||||
const manifest = exported.normalized_result;
|
||||
const scriptIndex = manifest.scripts.findIndex(
|
||||
(script) => script.url === `${site.origin}/scoped/main.js?build=2`,
|
||||
);
|
||||
assert.ok(
|
||||
scriptIndex >= 0,
|
||||
"Real capture did not retain main module response",
|
||||
);
|
||||
const mapPath = join(root, "import-map.json");
|
||||
await writeFile(mapPath, JSON.stringify(site.importMap));
|
||||
const input = {
|
||||
manifest_path: manifest.manifest.path,
|
||||
script_index: scriptIndex,
|
||||
importer_url: `${site.origin}/scoped/main.js?build=2#entry`,
|
||||
import_map: { path: mapPath, base_url: `${site.origin}/maps/` },
|
||||
};
|
||||
const before = [...site.requests];
|
||||
const cliEvidence = await cli(
|
||||
"trace-web-module-imports",
|
||||
input.manifest_path,
|
||||
String(scriptIndex),
|
||||
"--importer-url",
|
||||
input.importer_url,
|
||||
"--import-map-path",
|
||||
mapPath,
|
||||
"--import-map-base-url",
|
||||
input.import_map.base_url,
|
||||
);
|
||||
const cliResult = assertTrace(cliEvidence, oracle, site, scriptIndex);
|
||||
await client.connect(transport);
|
||||
const response = await client.callTool({
|
||||
name: "trace_web_module_imports",
|
||||
arguments: input,
|
||||
});
|
||||
assert.notEqual(response.isError, true, mcpTextValue(response));
|
||||
const mcpEvidence = parseEvidence(
|
||||
JSON.parse(mcpTextValue(response)).evidence,
|
||||
);
|
||||
const mcpResult = assertTrace(mcpEvidence, oracle, site, scriptIndex);
|
||||
assert.deepEqual(mcpResult, cliResult, "CLI and MCP module traces differ");
|
||||
assert.equal(
|
||||
cliEvidence.subject.digest.sha256,
|
||||
createHash("sha256").update(site.main).digest("hex"),
|
||||
);
|
||||
const mapBytes = await readFile(mapPath);
|
||||
assert.equal(
|
||||
cliResult.import_map.sha256,
|
||||
createHash("sha256").update(mapBytes).digest("hex"),
|
||||
);
|
||||
assert.deepEqual(
|
||||
site.requests,
|
||||
before,
|
||||
"Tracing refetched or executed a captured website dependency",
|
||||
);
|
||||
for (const surface of ["cli", "stdio_mcp"]) {
|
||||
const withoutMap =
|
||||
surface === "cli"
|
||||
? await cli(
|
||||
"trace-web-module-imports",
|
||||
input.manifest_path,
|
||||
String(scriptIndex),
|
||||
)
|
||||
: parseEvidence(
|
||||
JSON.parse(
|
||||
mcpTextValue(
|
||||
await client.callTool({
|
||||
name: "trace_web_module_imports",
|
||||
arguments: {
|
||||
manifest_path: input.manifest_path,
|
||||
script_index: scriptIndex,
|
||||
},
|
||||
}),
|
||||
),
|
||||
).evidence,
|
||||
);
|
||||
const result = webModuleTraceResultSchema.parse(
|
||||
withoutMap.normalized_result,
|
||||
);
|
||||
assert.equal(result.import_map, null);
|
||||
assert.equal(result.imports[0].resolution.state, "rejected");
|
||||
assert.equal(
|
||||
result.imports[2].resolution.url,
|
||||
`${site.origin}/query.js?v=1#one`,
|
||||
);
|
||||
}
|
||||
assert.deepEqual(
|
||||
site.requests,
|
||||
before,
|
||||
"Map-free tracing refetched an asset",
|
||||
);
|
||||
const warnings = await new NativeModuleResolver(env).resolve({
|
||||
importerUrl: input.importer_url,
|
||||
specifiers: ["alias"],
|
||||
importMap: {
|
||||
baseUrl: input.import_map.base_url,
|
||||
value: { imports: { alias: "./global.js", ignored: 42 } },
|
||||
},
|
||||
});
|
||||
assert.equal(warnings.ok, true, warnings.ok ? "" : warnings.error.message);
|
||||
assert.ok(
|
||||
warnings.value.diagnostics.length > 0,
|
||||
"Native import-map warnings were discarded",
|
||||
);
|
||||
assert.equal(
|
||||
warnings.value.resolutions[0].url,
|
||||
`${site.origin}/maps/global.js`,
|
||||
);
|
||||
const controller = new AbortController();
|
||||
let owned;
|
||||
const interrupted = await new NativeModuleResolver(env, async (options) => {
|
||||
owned = await chromium.launch(options);
|
||||
controller.abort();
|
||||
return owned;
|
||||
}).resolve(
|
||||
{
|
||||
importerUrl: input.importer_url,
|
||||
importMap: null,
|
||||
specifiers: ["./dep.js"],
|
||||
},
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
assert.equal(
|
||||
interrupted.ok,
|
||||
false,
|
||||
"Cancellation returned successful native resolution",
|
||||
);
|
||||
assert.equal(interrupted.error._tag, "AnalysisCancelledError");
|
||||
assert.equal(
|
||||
owned.isConnected(),
|
||||
false,
|
||||
"Cancelled native resolver left its owned browser connected",
|
||||
);
|
||||
const syntheticImporter = "http://app.invalid/main.js?build=1#entry";
|
||||
const syntheticMapBase = "https://maps.invalid/maps/map.json?x=1&y=2#base";
|
||||
for (const useMap of [false, true])
|
||||
for (const surface of ["cli", "stdio_mcp"]) {
|
||||
let evidence;
|
||||
if (surface === "cli")
|
||||
evidence = await cli(
|
||||
"trace-web-module-imports",
|
||||
input.manifest_path,
|
||||
String(scriptIndex),
|
||||
"--importer-url",
|
||||
syntheticImporter,
|
||||
...(useMap
|
||||
? [
|
||||
"--import-map-path",
|
||||
mapPath,
|
||||
"--import-map-base-url",
|
||||
syntheticMapBase,
|
||||
]
|
||||
: []),
|
||||
);
|
||||
else {
|
||||
const response = await client.callTool({
|
||||
name: "trace_web_module_imports",
|
||||
arguments: {
|
||||
manifest_path: input.manifest_path,
|
||||
script_index: scriptIndex,
|
||||
importer_url: syntheticImporter,
|
||||
...(useMap
|
||||
? { import_map: { path: mapPath, base_url: syntheticMapBase } }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
assert.notEqual(response.isError, true, mcpTextValue(response));
|
||||
evidence = parseEvidence(JSON.parse(mcpTextValue(response)).evidence);
|
||||
}
|
||||
const result = webModuleTraceResultSchema.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
if (useMap)
|
||||
assert.deepEqual(result.imports[0].resolution, {
|
||||
state: "resolved",
|
||||
url: "https://maps.invalid/maps/global.js",
|
||||
});
|
||||
else assert.equal(result.imports[0].resolution.state, "rejected");
|
||||
assert.equal(
|
||||
result.imports[2].resolution.url,
|
||||
"http://app.invalid/query.js?v=1#one",
|
||||
);
|
||||
assert.equal(
|
||||
evidence.raw_result.native_report.importer_url,
|
||||
syntheticImporter,
|
||||
);
|
||||
assert.equal(
|
||||
result.source.script.url,
|
||||
`${site.origin}/scoped/main.js?build=2`,
|
||||
);
|
||||
}
|
||||
const escapedBase = await new NativeModuleResolver(env).resolve({
|
||||
importerUrl: syntheticImporter,
|
||||
importMap: {
|
||||
baseUrl: "https://maps.invalid/maps/a&b/map.json?x=1&y=2#base",
|
||||
value: { imports: { alias: "./alias.js" } },
|
||||
},
|
||||
specifiers: ["alias"],
|
||||
});
|
||||
assert.equal(
|
||||
escapedBase.ok,
|
||||
true,
|
||||
escapedBase.ok ? "" : escapedBase.error.message,
|
||||
);
|
||||
assert.deepEqual(escapedBase.value.resolutions[0], {
|
||||
state: "resolved",
|
||||
url: "https://maps.invalid/maps/a&b/alias.js",
|
||||
});
|
||||
assert.deepEqual(
|
||||
site.requests,
|
||||
before,
|
||||
"Synthetic-context tracing refetched or executed a website asset",
|
||||
);
|
||||
return {
|
||||
independent_http_importer_and_https_map_base: true,
|
||||
cancellation_cleanup: true,
|
||||
native_warnings_retained: true,
|
||||
cli: true,
|
||||
stdio_mcp: true,
|
||||
public_trace_cases: 8,
|
||||
literal_imports: 11,
|
||||
computed_unknowns: 1,
|
||||
native_loading_oracle: true,
|
||||
independent_module_instances: 3,
|
||||
import_map_scopes: true,
|
||||
null_and_backtracking_errors: true,
|
||||
query_fragment_identity: true,
|
||||
source_identity_verified: true,
|
||||
map_identity_verified: true,
|
||||
implicit_refetches: 0,
|
||||
};
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
await site.close();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const assertTrace = (evidence, oracle, site, scriptIndex) => {
|
||||
const result = webModuleTraceResultSchema.parse(evidence.normalized_result);
|
||||
assert.equal(result.source.script_index, scriptIndex);
|
||||
assert.equal(result.parser.state, "parsed");
|
||||
assert.equal(result.imports.length, 12);
|
||||
assert.deepEqual(
|
||||
result.imports.slice(0, 11).map((item) => item.resolution),
|
||||
oracle.resolutions,
|
||||
);
|
||||
assert.deepEqual(result.imports[11].resolution, {
|
||||
state: "unknown",
|
||||
reason: "computed-specifier",
|
||||
});
|
||||
assert.ok(result.imports.every((item) => item.execution === "unknown"));
|
||||
assert.equal(
|
||||
result.imports[6].captured_candidates.length,
|
||||
0,
|
||||
"An unloaded lazy URL became a captured source",
|
||||
);
|
||||
for (const index of [2, 3]) {
|
||||
assert.ok(result.imports[index].captured_candidates.length > 0);
|
||||
assert.ok(
|
||||
result.imports[index].captured_candidates.every(
|
||||
(candidate) => candidate.match === "response-url-without-fragment",
|
||||
),
|
||||
);
|
||||
}
|
||||
assert.equal(result.imports[4].resolution.url, `${site.origin}/query.js?v=2`);
|
||||
assert.equal(
|
||||
result.importer.url,
|
||||
`${site.origin}/scoped/main.js?build=2#entry`,
|
||||
);
|
||||
assert.equal(result.engine.id, "chromium-native-module-resolver");
|
||||
return result;
|
||||
};
|
||||
|
||||
const loadingOracle = async (executable, site) => {
|
||||
const browser = await chromium.launch({
|
||||
executablePath: executable,
|
||||
headless: true,
|
||||
timeout: 15000,
|
||||
args: [
|
||||
"--disable-dev-shm-usage",
|
||||
...(process.env.REA_BROWSER_NO_SANDBOX === "true"
|
||||
? ["--no-sandbox"]
|
||||
: []),
|
||||
],
|
||||
});
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await page.goto(site.origin);
|
||||
await page.waitForFunction(
|
||||
() => globalThis.__moduleProof !== undefined,
|
||||
undefined,
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
const proof = await page.evaluate(() => globalThis.__moduleProof);
|
||||
assert.equal(proof.alias, "scoped");
|
||||
assert.equal(proof.pkg, "pkg");
|
||||
assert.equal(proof.same_module, true);
|
||||
assert.equal(proof.different_fragments, true);
|
||||
assert.equal(new Set(proof.instances).size, 3);
|
||||
assert.deepEqual(proof.urls, [
|
||||
`${site.origin}/query.js?v=1#one`,
|
||||
`${site.origin}/query.js?v=1#two`,
|
||||
`${site.origin}/query.js?v=2`,
|
||||
]);
|
||||
assert.ok(
|
||||
!site.requests.some(
|
||||
(url) =>
|
||||
url.includes("lazy") ||
|
||||
url.includes("escape") ||
|
||||
url.includes("blocked"),
|
||||
),
|
||||
);
|
||||
return proof;
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { webScriptExportResultSchema } from "../../dist/domain/webScriptExport.js";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../dist/domain/javascriptApplicationAnalysis.js";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../dist/domain/javascript/javascriptApplicationAnalysis.js";
|
||||
|
||||
/** Verify exact exported bytes and the independently authored source manifest. */
|
||||
export const assertWebScriptExport = async (evidence, capture, assets) => {
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { parseEvidence } from "../../dist/domain/evidence.js";
|
||||
import { webSourceLocationResultSchema } from "../../dist/domain/webSourceLocation.js";
|
||||
import { startBrowserSourceMapSite } from "../fixtures/browser-source-map-site.mjs";
|
||||
import { mcpTextValue } from "./mcp-verifier-results.mjs";
|
||||
|
||||
/** Verify real Chrome capture → export → original compiler point through packaged CLI/MCP. */
|
||||
export async function verifyBrowserSourceMap(
|
||||
executable,
|
||||
entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url)),
|
||||
) {
|
||||
const site = await startBrowserSourceMapSite();
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-browser-source-map-"));
|
||||
const env = {
|
||||
...process.env,
|
||||
REA_LOG_LEVEL: "silent",
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-provider/hopper",
|
||||
};
|
||||
const cli = async (...args) => {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[entrypoint, ...args, "--json"],
|
||||
{ env, timeout: 45000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
return parseEvidence(JSON.parse(stdout));
|
||||
};
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "source-map-real-e2e", version: "1" });
|
||||
try {
|
||||
const capture = await cli(
|
||||
"capture-browser-scenario",
|
||||
JSON.stringify({
|
||||
browser: { mode: "launch", executable_path: executable },
|
||||
start_url: { url: site.origin },
|
||||
actions: [
|
||||
{
|
||||
step_id: "ready",
|
||||
action: "wait_for",
|
||||
locator: {
|
||||
kind: "css",
|
||||
selector: '[data-ready="source-map proof"]',
|
||||
},
|
||||
state: "visible",
|
||||
timeout_ms: 10000,
|
||||
},
|
||||
],
|
||||
capture: { network: { response_body: true } },
|
||||
}),
|
||||
);
|
||||
const capturePath = join(root, "capture.json");
|
||||
await writeFile(capturePath, JSON.stringify(capture));
|
||||
const exported = await cli(
|
||||
"export-web-scripts",
|
||||
capturePath,
|
||||
join(root, "export"),
|
||||
);
|
||||
const manifest = exported.normalized_result;
|
||||
const manifestBytes = await readFile(manifest.manifest.path);
|
||||
const identity = {
|
||||
sha256: sha(manifestBytes),
|
||||
bytes: manifestBytes.length,
|
||||
};
|
||||
const scriptIndex = manifest.scripts.findIndex(
|
||||
(script) => script.url === `${site.origin}/app.js?build=7`,
|
||||
);
|
||||
assert.ok(
|
||||
scriptIndex >= 0,
|
||||
"Actual browser capture omitted compiled source",
|
||||
);
|
||||
const mapPath = join(root, "app.js.map");
|
||||
await writeFile(mapPath, site.map);
|
||||
const before = site.mapRequests();
|
||||
await client.connect(transport);
|
||||
let expected;
|
||||
for (const context of [
|
||||
site.origin,
|
||||
site.origin.replace("://", "://selected-user:selected-value@"),
|
||||
]) {
|
||||
const input = {
|
||||
manifest_path: manifest.manifest.path,
|
||||
script_index: scriptIndex,
|
||||
source_map: {
|
||||
path: mapPath,
|
||||
url: `${context}/maps/app.js.map?v=7#context`,
|
||||
},
|
||||
generated_position: site.position,
|
||||
};
|
||||
expected = await assertPublicPair(cli, client, {
|
||||
site,
|
||||
input,
|
||||
capture,
|
||||
identity,
|
||||
});
|
||||
}
|
||||
assert.equal(
|
||||
site.mapRequests(),
|
||||
before,
|
||||
"Point tracing fetched the selected map URL",
|
||||
);
|
||||
return {
|
||||
compiler: site.compiler,
|
||||
generated_sha256: sha(site.generated),
|
||||
source_map_sha256: sha(site.map),
|
||||
original_sha256: sha(site.original),
|
||||
public_cli: true,
|
||||
public_stdio_mcp: true,
|
||||
actual_chrome_capture: true,
|
||||
source_map_refetch: false,
|
||||
original_point: site.originalPosition,
|
||||
codec: expected.engine,
|
||||
runtime: expected.runtime,
|
||||
selected_inert_userinfo_context_preserved: true,
|
||||
public_point_cases: 4,
|
||||
};
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
await site.close();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
function assertPoint(evidence, { site, input, capture, identity }) {
|
||||
const result = webSourceLocationResultSchema.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
assert.equal(result.manifest.sha256, identity.sha256);
|
||||
assert.equal(result.manifest.bytes, identity.bytes);
|
||||
assert.equal(result.source.bytes, Buffer.byteLength(site.generated));
|
||||
assert.equal(result.source.sha256, sha(site.generated));
|
||||
assert.equal(result.source_map.sha256, sha(site.map));
|
||||
assert.equal(result.source_map.url, input.source_map.url);
|
||||
assert.equal(result.manifest.source_evidence_id, capture.evidence_id);
|
||||
assert.equal(result.execution, "unknown");
|
||||
assert.equal(result.source_authenticity, "unknown");
|
||||
assert.equal(result.matches.length, 1);
|
||||
const match = result.matches[0];
|
||||
assert.equal(match.state, "mapped");
|
||||
assert.deepEqual(match.original_position, {
|
||||
...site.originalPosition,
|
||||
offset: site.originalOffset,
|
||||
content_position: "verified-in-embedded-text",
|
||||
});
|
||||
assert.equal(
|
||||
match.resolved_url,
|
||||
new URL("../src/fixture.ts?v=7#original", input.source_map.url).href,
|
||||
);
|
||||
assert.equal(match.content.state, "embedded");
|
||||
assert.equal(match.content.text, site.original);
|
||||
assert.equal(match.content.utf8_sha256, sha(site.original));
|
||||
assert.ok(result.runtime.v8_heap_limit_bytes <= 256 * 1024 * 1024);
|
||||
assert.deepEqual(evidence.raw_result, { source_map_text: site.map });
|
||||
return result;
|
||||
}
|
||||
const sha = (text) => createHash("sha256").update(text).digest("hex");
|
||||
|
||||
async function assertPublicPair(cli, client, oracle) {
|
||||
const { input } = oracle;
|
||||
const evidence = await cli(
|
||||
"trace-web-source-location",
|
||||
input.manifest_path,
|
||||
String(input.script_index),
|
||||
input.source_map.path,
|
||||
input.source_map.url,
|
||||
String(input.generated_position.line),
|
||||
String(input.generated_position.column),
|
||||
);
|
||||
const expected = assertPoint(evidence, oracle);
|
||||
const response = await client.callTool({
|
||||
name: "trace_web_source_location",
|
||||
arguments: input,
|
||||
});
|
||||
assert.notEqual(response.isError, true, mcpTextValue(response));
|
||||
const mcpEvidence = parseEvidence(
|
||||
JSON.parse(mcpTextValue(response)).evidence,
|
||||
);
|
||||
assert.deepEqual(assertPoint(mcpEvidence, oracle), expected);
|
||||
assert.equal(evidence.parameters.source_map.url, input.source_map.url);
|
||||
return expected;
|
||||
}
|
||||
@@ -96,6 +96,21 @@ export const toolFamilyCatalog = (sources) => {
|
||||
|
||||
/** Build provider identities with their sorted capability names. */
|
||||
export const providerCatalog = (sources) => {
|
||||
const contractsByName = new Map(
|
||||
sources.toolContracts.TOOL_CONTRACTS.map((contract) => [
|
||||
contract.name,
|
||||
contract,
|
||||
]),
|
||||
);
|
||||
const declaredContracts = (providerId, operations) =>
|
||||
operations.map((operation) => {
|
||||
const contract = contractsByName.get(operation);
|
||||
if (contract === undefined)
|
||||
throw new TypeError(
|
||||
`Provider ${providerId} declares an unknown analyst operation: ${operation}`,
|
||||
);
|
||||
return contract;
|
||||
});
|
||||
const {
|
||||
applicationContracts,
|
||||
reconciliationContracts,
|
||||
@@ -105,15 +120,21 @@ export const providerCatalog = (sources) => {
|
||||
return [
|
||||
{
|
||||
identity: sources.hopperProvider.HOPPER_PROVIDER_IDENTITY,
|
||||
contracts: sources.hopperProvider.HOPPER_PROVIDER_TOOL_CONTRACTS,
|
||||
contracts: declaredContracts(
|
||||
"hopper",
|
||||
sources.hopperProvider.HOPPER_OPERATIONS,
|
||||
),
|
||||
},
|
||||
{
|
||||
identity: sources.ghidraProvider.GHIDRA_PROVIDER_IDENTITY,
|
||||
contracts: sources.ghidraProvider.GHIDRA_PROVIDER_TOOL_CONTRACTS,
|
||||
contracts: declaredContracts(
|
||||
"ghidra",
|
||||
sources.ghidraProvider.GHIDRA_OPERATIONS,
|
||||
),
|
||||
},
|
||||
{
|
||||
identity: sources.idaProvider.IDA_PROVIDER_IDENTITY,
|
||||
contracts: sources.idaProvider.IDA_PROVIDER_TOOL_CONTRACTS,
|
||||
contracts: declaredContracts("ida", sources.idaProvider.IDA_OPERATIONS),
|
||||
},
|
||||
{
|
||||
identity: sources.nativeProvider.NATIVE_MACOS_PROVIDER_IDENTITY,
|
||||
|
||||
@@ -42,25 +42,26 @@ const SOURCE_PATHS = {
|
||||
sessionContracts: "dist/contracts/sessionToolContracts.js",
|
||||
nativeContracts: "dist/contracts/nativeToolContracts.js",
|
||||
artifactContracts: "dist/contracts/artifactToolContracts.js",
|
||||
managedContracts: "dist/contracts/managedToolContracts.js",
|
||||
firmwareContracts: "dist/contracts/firmwareToolContracts.js",
|
||||
managedContracts: "dist/contracts/managed/managedToolContracts.js",
|
||||
firmwareContracts: "dist/contracts/firmware/firmwareToolContracts.js",
|
||||
firmwareProvider: "dist/firmware/FirmwareRelease.js",
|
||||
androidContracts: "dist/contracts/androidToolContracts.js",
|
||||
androidContracts: "dist/contracts/android/androidToolContracts.js",
|
||||
androidProvider: "dist/android/JadxRelease.js",
|
||||
managedWorkflowContracts: "dist/contracts/managedWorkflowToolContracts.js",
|
||||
managedWorkflowContracts:
|
||||
"dist/contracts/managed/managedWorkflowToolContracts.js",
|
||||
browserContracts: "dist/contracts/browserToolContracts.js",
|
||||
browserScenarioContracts: "dist/contracts/browserScenarioToolContracts.js",
|
||||
electronContracts: "dist/contracts/electronToolContracts.js",
|
||||
electronContracts: "dist/contracts/javascript/electronToolContracts.js",
|
||||
javascriptRuntimeObservationContracts:
|
||||
"dist/contracts/javascriptRuntimeObservationToolContracts.js",
|
||||
"dist/contracts/javascript/javascriptRuntimeObservationToolContracts.js",
|
||||
applicationContracts: "dist/contracts/applicationToolContracts.js",
|
||||
webScriptContracts: "dist/contracts/webScriptToolContracts.js",
|
||||
javascriptRecoveryContracts:
|
||||
"dist/contracts/javascriptRecoveryToolContracts.js",
|
||||
"dist/contracts/javascript/javascriptRecoveryToolContracts.js",
|
||||
javascriptRecoveryProvider: "dist/javascript/recovery/WakaruRelease.js",
|
||||
supportedClients: "dist/application/SupportedClients.js",
|
||||
hopperProvider: "dist/hopper/HopperProvider.js",
|
||||
ghidraProvider: "dist/ghidra/GhidraProvider.js",
|
||||
hopperProvider: "dist/hopper/HopperProviderCapabilities.js",
|
||||
ghidraProvider: "dist/ghidra/GhidraProviderCapabilities.js",
|
||||
idaProvider: "dist/ida/IdaProviderCapabilities.js",
|
||||
nativeProvider: "dist/native/NativeMacOSProviderMetadata.js",
|
||||
artifactProviders: "dist/application/InvestigationProviders.js",
|
||||
@@ -68,7 +69,7 @@ const SOURCE_PATHS = {
|
||||
browserScenarioProvider: "dist/browser/PlaywrightBrowserScenarioProvider.js",
|
||||
electronProvider: "dist/browser/CdpElectronProvider.js",
|
||||
electronActiveProvider: "dist/browser/PlaywrightElectronActiveProvider.js",
|
||||
v8InspectorProvider: "dist/browser/V8InspectorProvider.js",
|
||||
v8InspectorProvider: "dist/inspector/V8InspectorProvider.js",
|
||||
evidence: "dist/domain/evidence.js",
|
||||
evidenceBundle: "dist/domain/evidenceBundle.js",
|
||||
evidenceCompletion: "dist/domain/evidenceCompletionLedger.js",
|
||||
@@ -81,24 +82,31 @@ const SOURCE_PATHS = {
|
||||
browserScenarioCapture: "dist/domain/browserScenarioCapture.js",
|
||||
browserScenarioDiff: "dist/domain/browserScenarioDiff.js",
|
||||
browserSession: "dist/domain/browserSession.js",
|
||||
electronObservation: "dist/domain/electronObservation.js",
|
||||
electronActiveObservation: "dist/domain/electronActiveObservation.js",
|
||||
javascriptRuntimeObservation: "dist/domain/javascriptRuntimeObservation.js",
|
||||
electronObservation: "dist/domain/javascript/electronObservation.js",
|
||||
electronActiveObservation:
|
||||
"dist/domain/javascript/electronActiveObservation.js",
|
||||
javascriptRuntimeObservation:
|
||||
"dist/domain/javascript/javascriptRuntimeObservation.js",
|
||||
webBundleAnalysis: "dist/domain/webBundleAnalysis.js",
|
||||
webCaptureDiff: "dist/domain/webCaptureDiff.js",
|
||||
managedArtifact: "dist/domain/managedArtifact.js",
|
||||
managedComparison: "dist/domain/managedMemberComparison.js",
|
||||
managedNativeVerification: "dist/domain/managedNativeVerification.js",
|
||||
managedArtifact: "dist/domain/managed/managedArtifact.js",
|
||||
managedComparison: "dist/domain/managed/managedMemberComparison.js",
|
||||
managedNativeVerification: "dist/domain/managed/managedNativeVerification.js",
|
||||
webMcpDiscovery: "dist/domain/webMcpDiscovery.js",
|
||||
webScreenshot: "dist/domain/webScreenshot.js",
|
||||
javascriptApplicationGraph: "dist/domain/javascriptApplicationGraph.js",
|
||||
javascriptApplicationAnalysis: "dist/domain/javascriptApplicationAnalysis.js",
|
||||
javascriptSemanticGraph: "dist/domain/javascriptSemanticGraph.js",
|
||||
javascriptSemanticQuery: "dist/domain/javascriptSemanticQuerySchemas.js",
|
||||
javascriptSemanticTrace: "dist/domain/javascriptSemanticTraceSchemas.js",
|
||||
javascriptFeatureTrace: "dist/domain/javascriptFeatureTraceSchemas.js",
|
||||
javascriptApplicationGraph:
|
||||
"dist/domain/javascript/javascriptApplicationGraph.js",
|
||||
javascriptApplicationAnalysis:
|
||||
"dist/domain/javascript/javascriptApplicationAnalysis.js",
|
||||
javascriptSemanticGraph: "dist/domain/javascript/javascriptSemanticGraph.js",
|
||||
javascriptSemanticQuery:
|
||||
"dist/domain/javascript/javascriptSemanticQuerySchemas.js",
|
||||
javascriptSemanticTrace:
|
||||
"dist/domain/javascript/javascriptSemanticTraceSchemas.js",
|
||||
javascriptFeatureTrace:
|
||||
"dist/domain/javascript/javascriptFeatureTraceSchemas.js",
|
||||
javascriptVersionComparison:
|
||||
"dist/domain/javascriptApplicationVersionComparisonSchemas.js",
|
||||
"dist/domain/javascript/javascriptApplicationVersionComparisonSchemas.js",
|
||||
reconstructionVerification:
|
||||
"dist/domain/reconstructionVerificationSchemas.js",
|
||||
residualUnknown: "dist/domain/residualUnknown.js",
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
|
||||
/** Commands the macOS bundle fixture needs from Command Line Tools. */
|
||||
export const MACOS_BUNDLE_FIXTURE_COMMANDS = [
|
||||
["/usr/bin/xcrun", ["--find", "clang"]],
|
||||
["/usr/bin/xcrun", ["--find", "codesign"]],
|
||||
["/usr/bin/ditto", ["-h"]],
|
||||
["/usr/bin/hdiutil", ["help"]],
|
||||
];
|
||||
|
||||
/** Fail with the missing command name before building any fixture. */
|
||||
export async function preflightMacosBundleFixture() {
|
||||
if (process.platform !== "darwin")
|
||||
throw new Error(
|
||||
"macOS bundle verification requires a macOS host with Command Line Tools",
|
||||
);
|
||||
for (const [command, arguments_] of MACOS_BUNDLE_FIXTURE_COMMANDS)
|
||||
try {
|
||||
await exec(command, arguments_);
|
||||
} catch (cause) {
|
||||
// `ditto -h` and `hdiutil help` exit non-zero while printing usage.
|
||||
if (cause?.code === "ENOENT")
|
||||
throw new Error(
|
||||
`macOS bundle verification requires ${command} ${arguments_.join(" ")}`,
|
||||
{ cause },
|
||||
);
|
||||
if (command === "/usr/bin/xcrun")
|
||||
throw new Error(
|
||||
`macOS bundle verification requires Command Line Tools (${arguments_.join(" ")} failed)`,
|
||||
{ cause },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const plist = (entries) =>
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
${Object.entries(entries)
|
||||
.map(([key, value]) => ` <key>${key}</key>\n <string>${value}</string>`)
|
||||
.join("\n")}
|
||||
</dict>
|
||||
</plist>
|
||||
`;
|
||||
|
||||
const bundlePlist = (identifier, executable, packageType = "APPL") =>
|
||||
plist({
|
||||
CFBundleIdentifier: identifier,
|
||||
CFBundleExecutable: executable,
|
||||
CFBundlePackageType: packageType,
|
||||
});
|
||||
|
||||
const write = async (path, text) => {
|
||||
await mkdir(dirname(path), { recursive: true });
|
||||
await writeFile(path, text);
|
||||
};
|
||||
|
||||
/** Compile one C source beneath `root/sources` into `output`. */
|
||||
const clang = async (root, { name, source, output, flags = [] }) => {
|
||||
const sourcePath = join(root, "sources", `${name}.c`);
|
||||
await write(sourcePath, source);
|
||||
await mkdir(dirname(output), { recursive: true });
|
||||
await exec("/usr/bin/xcrun", [
|
||||
"clang",
|
||||
"-O0",
|
||||
sourcePath,
|
||||
"-o",
|
||||
output,
|
||||
...flags,
|
||||
]);
|
||||
};
|
||||
|
||||
const PROGRAM = `int main(void) { return 0; }\n`;
|
||||
|
||||
/** A versioned framework whose own rpath supplies a second `libchain.dylib`. */
|
||||
const buildCoreFramework = async (root, frameworks) => {
|
||||
const core = join(frameworks, "Core.framework");
|
||||
const version = join(core, "Versions", "A");
|
||||
const chainFlags = ["-dynamiclib", "-install_name", "@rpath/libchain.dylib"];
|
||||
await clang(root, {
|
||||
name: "chain-app",
|
||||
source: `const char *chain_variant(void) { return "app"; }\n`,
|
||||
output: join(frameworks, "libchain.dylib"),
|
||||
flags: chainFlags,
|
||||
});
|
||||
await clang(root, {
|
||||
name: "chain-framework",
|
||||
source: `const char *chain_variant(void) { return "framework"; }\n`,
|
||||
output: join(version, "Libraries", "libchain.dylib"),
|
||||
flags: chainFlags,
|
||||
});
|
||||
await clang(root, {
|
||||
name: "sibling",
|
||||
source: `int sibling_value(void) { return 7; }\n`,
|
||||
output: join(version, "libsibling.dylib"),
|
||||
flags: ["-dynamiclib", "-install_name", "@loader_path/libsibling.dylib"],
|
||||
});
|
||||
await clang(root, {
|
||||
name: "core",
|
||||
source: `const char *chain_variant(void);
|
||||
int sibling_value(void);
|
||||
const char *core_chain(void) { return sibling_value() == 7 ? chain_variant() : "broken"; }
|
||||
`,
|
||||
output: join(version, "Core"),
|
||||
flags: [
|
||||
"-dynamiclib",
|
||||
"-install_name",
|
||||
"@rpath/Core.framework/Versions/A/Core",
|
||||
"-Wl,-rpath,@loader_path/Libraries",
|
||||
join(version, "libsibling.dylib"),
|
||||
join(version, "Libraries", "libchain.dylib"),
|
||||
],
|
||||
});
|
||||
await write(
|
||||
join(version, "Resources", "Info.plist"),
|
||||
bundlePlist("com.example.rea.core", "Core", "FMWK"),
|
||||
);
|
||||
const service = join(version, "XPCServices", "FwSvc.xpc", "Contents");
|
||||
await clang(root, {
|
||||
name: "framework-service",
|
||||
source: PROGRAM,
|
||||
output: join(service, "MacOS", "FwSvc"),
|
||||
});
|
||||
await write(
|
||||
join(service, "Info.plist"),
|
||||
bundlePlist("com.example.rea.core.service", "FwSvc", "XPC!"),
|
||||
);
|
||||
await symlink("A", join(core, "Versions", "Current"));
|
||||
await symlink("Versions/Current/Core", join(core, "Core"));
|
||||
await symlink("Versions/Current/Resources", join(core, "Resources"));
|
||||
await symlink("Versions/Current/XPCServices", join(core, "XPCServices"));
|
||||
return join(version, "Core");
|
||||
};
|
||||
|
||||
/** The main executable, with a weak dependency deleted after linking. */
|
||||
const buildMainExecutable = async (root, contents, coreBinary) => {
|
||||
const gone = join(contents, "Frameworks", "libgone.dylib");
|
||||
await clang(root, {
|
||||
name: "gone",
|
||||
source: `int gone_value(void) { return 1; }\n`,
|
||||
output: gone,
|
||||
flags: ["-dynamiclib", "-install_name", "@rpath/libgone.dylib"],
|
||||
});
|
||||
const executable = join(contents, "MacOS", "MacFixture");
|
||||
await clang(root, {
|
||||
name: "main",
|
||||
source: `#include <stdio.h>
|
||||
const char *core_chain(void);
|
||||
extern int gone_value(void) __attribute__((weak_import));
|
||||
int main(void) {
|
||||
printf("chain=%s weak=%s\\n", core_chain(), gone_value ? "present" : "absent");
|
||||
return 0;
|
||||
}
|
||||
`,
|
||||
output: executable,
|
||||
flags: [
|
||||
"-Wl,-rpath,@executable_path/../Frameworks",
|
||||
coreBinary,
|
||||
`-Wl,-weak_library,${gone}`,
|
||||
],
|
||||
});
|
||||
await rm(gone);
|
||||
await write(
|
||||
join(contents, "Info.plist"),
|
||||
bundlePlist("com.example.rea.fixture", "MacFixture"),
|
||||
);
|
||||
return executable;
|
||||
};
|
||||
|
||||
/** One `Contents/`-layout bundle with a trivial executable. */
|
||||
const buildNestedBundle = async (root, { bundle, executable, plist }) => {
|
||||
await clang(root, {
|
||||
name: executable,
|
||||
source: PROGRAM,
|
||||
output: join(bundle, "Contents", "MacOS", executable),
|
||||
});
|
||||
await write(join(bundle, "Contents", "Info.plist"), plist);
|
||||
};
|
||||
|
||||
const buildNestedBundles = async (root, contents, coreBinary) => {
|
||||
const service = join(contents, "XPCServices", "Svc.xpc", "Contents");
|
||||
await clang(root, {
|
||||
name: "service",
|
||||
source: `const char *core_chain(void);
|
||||
int main(void) { return core_chain()[0] == 'b'; }
|
||||
`,
|
||||
output: join(service, "MacOS", "Svc"),
|
||||
flags: ["-Wl,-rpath,@executable_path/../../../../Frameworks", coreBinary],
|
||||
});
|
||||
await write(
|
||||
join(service, "Info.plist"),
|
||||
bundlePlist("com.example.rea.fixture.service", "Svc", "XPC!"),
|
||||
);
|
||||
await buildNestedBundle(root, {
|
||||
bundle: join(contents, "PlugIns", "Ext.appex"),
|
||||
executable: "Ext",
|
||||
plist: bundlePlist("com.example.rea.fixture.extension", "Ext", "XPC!"),
|
||||
});
|
||||
await buildNestedBundle(root, {
|
||||
bundle: join(contents, "Library", "LoginItems", "Login.app"),
|
||||
executable: "Login",
|
||||
plist: bundlePlist("com.example.rea.fixture.login", "Login"),
|
||||
});
|
||||
};
|
||||
|
||||
/** A privileged helper, its launchd plists, and a non-bundle helper tool. */
|
||||
const buildLaunchItems = async (root, contents) => {
|
||||
await clang(root, {
|
||||
name: "privileged-helper",
|
||||
source: PROGRAM,
|
||||
output: join(
|
||||
contents,
|
||||
"Library",
|
||||
"LaunchServices",
|
||||
"com.example.rea.helper",
|
||||
),
|
||||
});
|
||||
await write(
|
||||
join(contents, "Library", "LaunchDaemons", "com.example.rea.helper.plist"),
|
||||
plist({
|
||||
Label: "com.example.rea.helper",
|
||||
BundleProgram: "Contents/Library/LaunchServices/com.example.rea.helper",
|
||||
}),
|
||||
);
|
||||
await write(
|
||||
join(contents, "Library", "LaunchAgents", "com.example.rea.agent.plist"),
|
||||
plist({
|
||||
Label: "com.example.rea.agent",
|
||||
BundleProgram: "Contents/Helpers/rea-tool",
|
||||
}),
|
||||
);
|
||||
await clang(root, {
|
||||
name: "tool",
|
||||
source: PROGRAM,
|
||||
output: join(contents, "Helpers", "rea-tool"),
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Build `MacFixture.app` with every bundle role and dylib-loading shape REA
|
||||
* projects. The main executable prints which `libchain.dylib` copy dyld
|
||||
* selected, so callers can compare static resolution with a real load.
|
||||
*/
|
||||
export async function buildMacosBundleFixture(root) {
|
||||
const app = join(root, "MacFixture.app");
|
||||
const contents = join(app, "Contents");
|
||||
const coreBinary = await buildCoreFramework(
|
||||
root,
|
||||
join(contents, "Frameworks"),
|
||||
);
|
||||
const executable = await buildMainExecutable(root, contents, coreBinary);
|
||||
await buildNestedBundles(root, contents, coreBinary);
|
||||
await buildLaunchItems(root, contents);
|
||||
await exec("/usr/bin/xcrun", [
|
||||
"codesign",
|
||||
"--force",
|
||||
"--deep",
|
||||
"--sign",
|
||||
"-",
|
||||
app,
|
||||
]);
|
||||
return { app, executable };
|
||||
}
|
||||
@@ -5,9 +5,11 @@ export interface ModuleBoundaryViolation {
|
||||
readonly specifier: string;
|
||||
readonly target: string;
|
||||
readonly boundary:
|
||||
| "artifact-acquisition"
|
||||
| "pure-layer"
|
||||
| "application-composition"
|
||||
| "provider-construction";
|
||||
| "provider-construction"
|
||||
| "provider-generated-catalog";
|
||||
}
|
||||
|
||||
/** Check resolved source ownership, including type imports and reexports. */
|
||||
|
||||
@@ -10,8 +10,16 @@ const MIGRATED_PROVIDER_ROOTS = new Set([
|
||||
"ghidra",
|
||||
"hopper",
|
||||
"ida",
|
||||
"inspector",
|
||||
"javascript",
|
||||
]);
|
||||
const PROVIDER_ROOTS = new Set([
|
||||
...MIGRATED_PROVIDER_ROOTS,
|
||||
"artifacts",
|
||||
"browser",
|
||||
"dotnet",
|
||||
"native",
|
||||
]);
|
||||
|
||||
const repositoryPath = (root, path) =>
|
||||
relative(root, path).replaceAll("\\", "/");
|
||||
@@ -52,6 +60,27 @@ const failedBoundary = (file, target) => {
|
||||
if (!file.startsWith("src/") || !target.startsWith("src/")) return undefined;
|
||||
const layer = file.split("/")[1];
|
||||
const targetLayer = target.split("/")[1];
|
||||
if (
|
||||
file.startsWith("src/application/javascript/") &&
|
||||
/^src\/artifacts\/(?:Asar|Directory)ArtifactReader\.(?:js|ts)$/u.test(
|
||||
target,
|
||||
)
|
||||
)
|
||||
return "provider-construction";
|
||||
if (
|
||||
(file.startsWith("src/artifacts/javascript/") ||
|
||||
/^src\/artifacts\/ArtifactHash\.(?:js|ts)$/u.test(file)) &&
|
||||
(["application", "composition", "server", "cli", "main"].includes(
|
||||
targetLayer,
|
||||
) ||
|
||||
/^src\/(?:cli|main)\./u.test(target))
|
||||
)
|
||||
return "artifact-acquisition";
|
||||
if (
|
||||
PROVIDER_ROOTS.has(layer) &&
|
||||
/^src\/generatedMcpToolCatalog\.(?:js|ts)$/u.test(target)
|
||||
)
|
||||
return "provider-generated-catalog";
|
||||
if (PURE_LAYERS.has(layer)) {
|
||||
if (
|
||||
(/^src\/[^/]+\//u.test(target) && !PURE_LAYERS.has(targetLayer)) ||
|
||||
|
||||
+9
-1
@@ -41,6 +41,7 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
|
||||
const { createCli } = await import("../dist/cli.js");
|
||||
const {
|
||||
renderCliOutputArgumentError,
|
||||
renderEmptyFilteredCliOutput,
|
||||
sanitizeCliOutput,
|
||||
validateCliOutputArguments,
|
||||
} = await import("../dist/cliOutput.js");
|
||||
@@ -49,9 +50,16 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
|
||||
process.stdout.write(renderCliOutputArgumentError(outputArguments));
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
let wroteOutput = false;
|
||||
await createCli().serve(args, {
|
||||
stdout: (output) => process.stdout.write(sanitizeCliOutput(output)),
|
||||
stdout: (output) => {
|
||||
const sanitized = sanitizeCliOutput(output);
|
||||
if (sanitized.length > 0) wroteOutput = true;
|
||||
process.stdout.write(sanitized);
|
||||
},
|
||||
});
|
||||
if (!wroteOutput)
|
||||
process.stdout.write(renderEmptyFilteredCliOutput(args) ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+181
-109
@@ -12,118 +12,190 @@ if (process.platform !== "darwin")
|
||||
"Apple dispatch verification requires macOS with a host Objective-C compiler and Foundation SDK",
|
||||
);
|
||||
const exec = promisify(execFile);
|
||||
const source = (name) =>
|
||||
fileURLToPath(
|
||||
new URL(`../tests/conformance/native/${name}`, import.meta.url),
|
||||
);
|
||||
|
||||
/** Both link modes: legacy LC_DYLD_INFO binds and the default chained fixups. */
|
||||
const LINK_MODES = [
|
||||
{
|
||||
mode: "dyld-info",
|
||||
flags: ["-Wl,-no_fixup_chains"],
|
||||
fixups: "LC_DYLD_INFO bind opcodes",
|
||||
swift: true,
|
||||
},
|
||||
{ mode: "chained", flags: [], fixups: "chained fixups:", swift: true },
|
||||
// arm64e uses authenticated chained pointers (DYLD_CHAINED_PTR_ARM64E_*).
|
||||
...(process.arch === "arm64"
|
||||
? [
|
||||
{
|
||||
mode: "chained-arm64e",
|
||||
flags: ["-arch", "arm64e"],
|
||||
fixups: "chained fixups: DYLD_CHAINED_PTR_ARM64E",
|
||||
swift: false,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
const fixupCoverage = (metadata) =>
|
||||
metadata.coverage.find(({ facet }) => facet === "pointer_fixups");
|
||||
|
||||
const inspect = async (path) => {
|
||||
const target = await parseBinaryTarget(path);
|
||||
if (!target.ok) throw target.error;
|
||||
return {
|
||||
target: target.value,
|
||||
metadata: (await inspectAppleDispatchMetadata(target.value, 5000)).result,
|
||||
};
|
||||
};
|
||||
|
||||
const checkObjc = (metadata, mode) => {
|
||||
const fixture = metadata.objc_classes.find(
|
||||
({ name, is_meta_class: meta }) => name === "ReaDispatchFixture" && !meta,
|
||||
);
|
||||
const category = metadata.objc_categories.find(
|
||||
({ name }) => name === "ReaDispatchAdditions",
|
||||
);
|
||||
const objcCoverage = metadata.coverage.find(
|
||||
({ facet }) => facet === "objc_class_method_ivar_metadata",
|
||||
);
|
||||
if (
|
||||
!metadata.objc_dispatch_implementations.some(
|
||||
(item) =>
|
||||
item.class_name === "ReaDispatchFixture" &&
|
||||
item.selector === "performAction:" &&
|
||||
item.implementation_address !== null,
|
||||
) ||
|
||||
!metadata.objc_dispatch_implementations.some(
|
||||
(item) =>
|
||||
item.selector === "fixtureVersion" && item.method_type === "class",
|
||||
) ||
|
||||
!metadata.objc_ivars.some(
|
||||
(item) => item.name === "state" && item.offset !== null,
|
||||
) ||
|
||||
!metadata.objc_protocol_records.some(
|
||||
(protocol) =>
|
||||
protocol.name === "ReaDispatchProtocol" &&
|
||||
protocol.methods.some(
|
||||
(method) => method.selector === "performAction:",
|
||||
) &&
|
||||
protocol.optional_methods.some(
|
||||
(method) => method.selector === "optionalFixtureValue",
|
||||
),
|
||||
) ||
|
||||
// The superclass lives in libobjc, so it comes from a bind fixup.
|
||||
fixture?.super_class !== "NSObject" ||
|
||||
fixture.decode.status !== "decoded" ||
|
||||
!fixture.properties.some(
|
||||
(property) =>
|
||||
property.name === "fixtureName" &&
|
||||
property.type_encoding === '@"NSString"' &&
|
||||
property.attributes.some(({ name }) => name === "C"),
|
||||
) ||
|
||||
category?.class_name !== "NSString" ||
|
||||
category.class_source !== "external" ||
|
||||
!category.instance_methods.includes("reaFixtureLength") ||
|
||||
!metadata.objc_dispatch_implementations.some(
|
||||
(item) =>
|
||||
item.class_name === "NSString" &&
|
||||
item.category === "ReaDispatchAdditions" &&
|
||||
item.selector === "reaFixtureLength",
|
||||
) ||
|
||||
objcCoverage?.status !== "complete" ||
|
||||
fixupCoverage(metadata)?.reason?.startsWith(mode.fixups) !== true
|
||||
)
|
||||
throw new Error(
|
||||
`Apple metadata fixture drifted (${mode.mode}): ${JSON.stringify(metadata)}`,
|
||||
);
|
||||
};
|
||||
|
||||
const checkSwift = (metadata, mode) => {
|
||||
if (
|
||||
!metadata.swift_dispatch_slots.some(
|
||||
(slot) =>
|
||||
slot.table_kind === "class_vtable" &&
|
||||
slot.owner === "ReaVtableFixture" &&
|
||||
slot.decode.status === "decoded",
|
||||
)
|
||||
)
|
||||
throw new Error(
|
||||
`Swift class vtable fixture unavailable (${mode.mode}): ${JSON.stringify(metadata.coverage)}`,
|
||||
);
|
||||
if (
|
||||
!metadata.swift_conformances.some(
|
||||
(item) =>
|
||||
item.type_name === "ReaWitnessFixture" &&
|
||||
item.protocol_name === "ReaWitnessFixtureProtocol",
|
||||
) ||
|
||||
!metadata.swift_dispatch_slots.some(
|
||||
(item) =>
|
||||
item.owner === "ReaWitnessFixture: ReaWitnessFixtureProtocol" &&
|
||||
item.implementation_address !== null,
|
||||
) ||
|
||||
fixupCoverage(metadata)?.reason?.startsWith(mode.fixups) !== true
|
||||
)
|
||||
throw new Error(
|
||||
`Swift metadata fixture drifted (${mode.mode}): ${JSON.stringify(metadata)}`,
|
||||
);
|
||||
};
|
||||
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-dispatch-"));
|
||||
try {
|
||||
const targetPath = join(root, "fixture");
|
||||
await exec("/usr/bin/xcrun", [
|
||||
"clang",
|
||||
"-O2",
|
||||
"-framework",
|
||||
"Foundation",
|
||||
"-Wl,-no_fixup_chains",
|
||||
fileURLToPath(
|
||||
new URL("../tests/conformance/native/dispatch.m", import.meta.url),
|
||||
),
|
||||
"-o",
|
||||
targetPath,
|
||||
]);
|
||||
const results = [];
|
||||
for (const variant of ["symbols", "stripped"]) {
|
||||
if (variant === "stripped")
|
||||
await exec("/usr/bin/strip", ["-x", targetPath]);
|
||||
const target = await parseBinaryTarget(targetPath);
|
||||
if (!target.ok) throw target.error;
|
||||
const observation = await inspectAppleDispatchMetadata(target.value, 5000);
|
||||
const metadata = observation.result;
|
||||
if (
|
||||
!metadata.objc_dispatch_implementations.some(
|
||||
(item) =>
|
||||
item.class_name === "ReaDispatchFixture" &&
|
||||
item.selector === "performAction:" &&
|
||||
item.implementation_address !== null,
|
||||
) ||
|
||||
!metadata.objc_dispatch_implementations.some(
|
||||
(item) =>
|
||||
item.selector === "fixtureVersion" && item.method_type === "class",
|
||||
) ||
|
||||
!metadata.objc_ivars.some(
|
||||
(item) => item.name === "state" && item.offset !== null,
|
||||
) ||
|
||||
!metadata.objc_protocol_records.some(
|
||||
(protocol) =>
|
||||
protocol.name === "ReaDispatchProtocol" &&
|
||||
protocol.methods.some(
|
||||
(method) => method.selector === "performAction:",
|
||||
) &&
|
||||
protocol.optional_methods.some(
|
||||
(method) => method.selector === "optionalFixtureValue",
|
||||
),
|
||||
)
|
||||
)
|
||||
throw new Error(
|
||||
`Apple metadata fixture drifted: ${JSON.stringify(observation)}`,
|
||||
);
|
||||
results.push({
|
||||
variant,
|
||||
target_sha256: target.value.sha256,
|
||||
implementations: metadata.objc_dispatch_implementations.length,
|
||||
ivars: metadata.objc_ivars.length,
|
||||
coverage: metadata.coverage,
|
||||
});
|
||||
}
|
||||
const swiftPath = join(root, "libReaWitnessFixture.dylib");
|
||||
await exec("/usr/bin/xcrun", [
|
||||
"swiftc",
|
||||
"-emit-library",
|
||||
"-O",
|
||||
"-Xlinker",
|
||||
"-no_fixup_chains",
|
||||
fileURLToPath(
|
||||
new URL("../tests/conformance/native/dispatch.swift", import.meta.url),
|
||||
),
|
||||
"-o",
|
||||
swiftPath,
|
||||
]);
|
||||
for (const variant of ["swift-symbols", "swift-stripped"]) {
|
||||
if (variant === "swift-stripped")
|
||||
await exec("/usr/bin/strip", ["-x", swiftPath]);
|
||||
const target = await parseBinaryTarget(swiftPath);
|
||||
if (!target.ok) throw target.error;
|
||||
const observation = await inspectAppleDispatchMetadata(target.value, 5000);
|
||||
const metadata = observation.result;
|
||||
if (
|
||||
!metadata.swift_dispatch_slots.some(
|
||||
(slot) =>
|
||||
slot.table_kind === "class_vtable" &&
|
||||
slot.owner === "ReaVtableFixture" &&
|
||||
slot.decode.status === "decoded",
|
||||
)
|
||||
)
|
||||
throw new Error(
|
||||
`Swift class vtable fixture unavailable: ${JSON.stringify(metadata.coverage)}`,
|
||||
);
|
||||
if (
|
||||
!metadata.swift_conformances.some(
|
||||
(item) =>
|
||||
item.type_name === "ReaWitnessFixture" &&
|
||||
item.protocol_name === "ReaWitnessFixtureProtocol",
|
||||
) ||
|
||||
!metadata.swift_dispatch_slots.some(
|
||||
(item) =>
|
||||
item.owner === "ReaWitnessFixture: ReaWitnessFixtureProtocol" &&
|
||||
item.implementation_address !== null,
|
||||
)
|
||||
)
|
||||
throw new Error(
|
||||
`Swift metadata fixture drifted: ${JSON.stringify(observation)}`,
|
||||
);
|
||||
results.push({
|
||||
variant,
|
||||
target_sha256: target.value.sha256,
|
||||
conformances: metadata.swift_conformances.length,
|
||||
slots: metadata.swift_dispatch_slots.length,
|
||||
coverage: metadata.coverage,
|
||||
});
|
||||
for (const mode of LINK_MODES) {
|
||||
const objcPath = join(root, `fixture-${mode.mode}`);
|
||||
await exec("/usr/bin/xcrun", [
|
||||
"clang",
|
||||
"-O2",
|
||||
"-framework",
|
||||
"Foundation",
|
||||
...mode.flags,
|
||||
source("dispatch.m"),
|
||||
"-o",
|
||||
objcPath,
|
||||
]);
|
||||
for (const variant of ["symbols", "stripped"]) {
|
||||
if (variant === "stripped")
|
||||
await exec("/usr/bin/strip", ["-x", objcPath]);
|
||||
const { target, metadata } = await inspect(objcPath);
|
||||
checkObjc(metadata, mode);
|
||||
results.push({
|
||||
variant: `objc-${mode.mode}-${variant}`,
|
||||
target_sha256: target.sha256,
|
||||
classes: metadata.objc_classes.length,
|
||||
categories: metadata.objc_categories.length,
|
||||
implementations: metadata.objc_dispatch_implementations.length,
|
||||
fixups: fixupCoverage(metadata)?.reason,
|
||||
});
|
||||
}
|
||||
if (!mode.swift) continue;
|
||||
const swiftPath = join(root, `libReaWitnessFixture-${mode.mode}.dylib`);
|
||||
await exec("/usr/bin/xcrun", [
|
||||
"swiftc",
|
||||
"-emit-library",
|
||||
"-O",
|
||||
...mode.flags
|
||||
.map((flag) => flag.replace(/^-Wl,/u, "-Xlinker\0"))
|
||||
.flatMap((flag) => flag.split("\0")),
|
||||
source("dispatch.swift"),
|
||||
"-o",
|
||||
swiftPath,
|
||||
]);
|
||||
for (const variant of ["symbols", "stripped"]) {
|
||||
if (variant === "stripped")
|
||||
await exec("/usr/bin/strip", ["-x", swiftPath]);
|
||||
const { target, metadata } = await inspect(swiftPath);
|
||||
checkSwift(metadata, mode);
|
||||
results.push({
|
||||
variant: `swift-${mode.mode}-${variant}`,
|
||||
target_sha256: target.sha256,
|
||||
conformances: metadata.swift_conformances.length,
|
||||
slots: metadata.swift_dispatch_slots.length,
|
||||
fixups: fixupCoverage(metadata)?.reason,
|
||||
});
|
||||
}
|
||||
}
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ ok: true, target_executed: false, fixtures: results })}\n`,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
import { access } from "node:fs/promises";
|
||||
import { verifyBrowserModules } from "./lib/browser-module-e2e.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
|
||||
const executable = process.env.REA_BROWSER_EXECUTABLE;
|
||||
if (!executable)
|
||||
throw new Error(
|
||||
"verify:browser:modules requires REA_BROWSER_EXECUTABLE for caller-supplied Chromium",
|
||||
);
|
||||
await access(executable);
|
||||
const run = createVerifierRun();
|
||||
const proof = await verifyBrowserModules(executable, process.argv[2]);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ ...proof, verifier_run: await completeVerifierRun(run), verified: true })}\n`,
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
import { access } from "node:fs/promises";
|
||||
import { isAbsolute } from "node:path";
|
||||
import { verifyBrowserSourceMap } from "./lib/browser-source-map-e2e.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
const executable = process.env.REA_BROWSER_EXECUTABLE;
|
||||
if (!executable || !isAbsolute(executable))
|
||||
throw new Error(
|
||||
"verify:browser:source-maps requires absolute REA_BROWSER_EXECUTABLE pointing to caller-supplied Chromium",
|
||||
);
|
||||
await access(executable);
|
||||
const run = createVerifierRun();
|
||||
const proof = await verifyBrowserSourceMap(executable, process.argv[2]);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ ...proof, verifier_run: await completeVerifierRun(run), verified: true })}\n`,
|
||||
);
|
||||
@@ -117,8 +117,41 @@ try {
|
||||
).length < 2
|
||||
)
|
||||
throw new Error("Real shared object identity was not preserved");
|
||||
const uidRoot = join(root, "uid-root.plist");
|
||||
await promisify(execFile)("/usr/bin/xcrun", [
|
||||
"swift",
|
||||
"-module-cache-path",
|
||||
join(root, "modules"),
|
||||
fileURLToPath(
|
||||
new URL(
|
||||
"../tests/conformance/native/keyed-archive-uid-root.swift",
|
||||
import.meta.url,
|
||||
),
|
||||
),
|
||||
uidRoot,
|
||||
]);
|
||||
const uidRootXml = join(root, "uid-root.xml.plist");
|
||||
await promisify(execFile)("/usr/bin/plutil", [
|
||||
"-convert",
|
||||
"xml1",
|
||||
"-o",
|
||||
uidRootXml,
|
||||
uidRoot,
|
||||
]);
|
||||
for (const path of [uidRoot, uidRootXml]) {
|
||||
const uidGraph = await artifactCli("inspect-keyed-archive", path);
|
||||
assert.deepEqual(
|
||||
uidGraph.references.map(({ source, path, target, status }) => ({
|
||||
source,
|
||||
path,
|
||||
target,
|
||||
status,
|
||||
})),
|
||||
[{ source: null, path: ["UID"], target: 1, status: "resolved" }],
|
||||
);
|
||||
}
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ ok: true, mocked: false, cli: true, stdio_mcp: true, xml_golden: true, format: graph.archive_format, objects: graph.total_objects, references: graph.total_references, shared_identity: true, cyclic_identity: true, target_classes_instantiated_by_reader: false })}\n`,
|
||||
`${JSON.stringify({ ok: true, mocked: false, cli: true, stdio_mcp: true, xml_golden: true, uid_named_root: true, format: graph.archive_format, objects: graph.total_objects, references: graph.total_references, shared_identity: true, cyclic_identity: true, target_classes_instantiated_by_reader: false })}\n`,
|
||||
);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import {
|
||||
artifactCliEvidence,
|
||||
artifactMcpResult,
|
||||
withArtifactMcp,
|
||||
} from "./lib/artifact-e2e.mjs";
|
||||
import {
|
||||
buildMacosBundleFixture,
|
||||
preflightMacosBundleFixture,
|
||||
} from "./lib/macos-bundle-fixture.mjs";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
|
||||
await preflightMacosBundleFixture();
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-macos-bundle-"));
|
||||
|
||||
/** Bundle roles relative to the application root, independent of the container. */
|
||||
const anatomy = (projection, applicationRoot) => {
|
||||
const relative = (path) =>
|
||||
applicationRoot === "."
|
||||
? path
|
||||
: path === applicationRoot
|
||||
? "."
|
||||
: path.slice(applicationRoot.length + 1);
|
||||
const paths = (components) =>
|
||||
components.map(({ path }) => relative(path)).sort();
|
||||
return {
|
||||
bundles: projection.bundles.map((bundle) => ({
|
||||
path: relative(bundle.path),
|
||||
role: bundle.role,
|
||||
layout: bundle.layout,
|
||||
parent: bundle.parent_path === null ? null : relative(bundle.parent_path),
|
||||
info_plist: bundle.info_plist_path && relative(bundle.info_plist_path),
|
||||
executables: bundle.executable_candidates.map(relative),
|
||||
signing: bundle.signing_paths.map(relative),
|
||||
})),
|
||||
privileged_helpers: paths(projection.components.privileged_helpers),
|
||||
launchd_plists: projection.components.launchd_plists
|
||||
.map(({ path, domain }) => `${domain}:${relative(path)}`)
|
||||
.sort(),
|
||||
helpers: paths(projection.components.helpers),
|
||||
};
|
||||
};
|
||||
|
||||
const FRAMEWORK = "Contents/Frameworks/Core.framework";
|
||||
const EXPECTED_BUNDLES = [
|
||||
[".", "application", "macos-deep", null],
|
||||
[FRAMEWORK, "framework", "versioned-framework", "."],
|
||||
[
|
||||
`${FRAMEWORK}/Versions/A/XPCServices/FwSvc.xpc`,
|
||||
"xpc-service",
|
||||
"macos-deep",
|
||||
FRAMEWORK,
|
||||
],
|
||||
["Contents/Library/LoginItems/Login.app", "login-item", "macos-deep", "."],
|
||||
["Contents/PlugIns/Ext.appex", "app-extension", "macos-deep", "."],
|
||||
["Contents/XPCServices/Svc.xpc", "xpc-service", "macos-deep", "."],
|
||||
];
|
||||
|
||||
try {
|
||||
const { app } = await buildMacosBundleFixture(root);
|
||||
const zip = join(root, "MacFixture.zip");
|
||||
await exec("/usr/bin/ditto", ["-c", "-k", "--keepParent", app, zip]);
|
||||
const staging = join(root, "dmg-source");
|
||||
await mkdir(staging);
|
||||
await exec("/usr/bin/ditto", [app, join(staging, "MacFixture.app")]);
|
||||
await symlink("/Applications", join(staging, "Applications"));
|
||||
const dmg = join(root, "MacFixture.dmg");
|
||||
// The default APFS image attaches as an image disk plus a synthesized
|
||||
// container, which exercises detach of devices sharing one image.
|
||||
await exec("/usr/bin/hdiutil", [
|
||||
"create",
|
||||
"-quiet",
|
||||
"-srcfolder",
|
||||
staging,
|
||||
"-volname",
|
||||
"ReaFixture",
|
||||
"-format",
|
||||
"UDZO",
|
||||
dmg,
|
||||
]);
|
||||
|
||||
const containers = [
|
||||
{ target: app, format: "directory", applicationRoot: "." },
|
||||
{ target: zip, format: "zip", applicationRoot: "MacFixture.app" },
|
||||
{
|
||||
target: dmg,
|
||||
format: "dmg",
|
||||
applicationRoot: "MacFixture.dmg/ReaFixture/MacFixture.app",
|
||||
},
|
||||
];
|
||||
const anatomies = [];
|
||||
for (const { target, format, applicationRoot } of containers) {
|
||||
const inventory = await artifactCliEvidence("inspect-artifact", target);
|
||||
const input = join(root, `${format}-projection.json`);
|
||||
await writeFile(input, JSON.stringify({ inventory_evidence: [inventory] }));
|
||||
const evidence = await artifactCliEvidence(
|
||||
"project-apple-application-graph",
|
||||
input,
|
||||
);
|
||||
const projection = evidence.normalized_result;
|
||||
assert.equal(evidence.subject?.format, format);
|
||||
assert.equal(projection.root_format, format);
|
||||
assert.deepEqual(projection.platforms, ["macos"]);
|
||||
assert.deepEqual(projection.application_roots, [applicationRoot]);
|
||||
assert.equal(projection.coverage.status, "complete-within-inventory");
|
||||
const observed = anatomy(projection, applicationRoot);
|
||||
assert.deepEqual(
|
||||
observed.bundles.map(({ path, role, layout, parent }) => [
|
||||
path,
|
||||
role,
|
||||
layout,
|
||||
parent,
|
||||
]),
|
||||
EXPECTED_BUNDLES,
|
||||
`${format} bundle roles drifted`,
|
||||
);
|
||||
assert.deepEqual(observed.privileged_helpers, [
|
||||
"Contents/Library/LaunchServices/com.example.rea.helper",
|
||||
]);
|
||||
assert.deepEqual(observed.launchd_plists, [
|
||||
"agent:Contents/Library/LaunchAgents/com.example.rea.agent.plist",
|
||||
"daemon:Contents/Library/LaunchDaemons/com.example.rea.helper.plist",
|
||||
]);
|
||||
assert.deepEqual(observed.helpers, ["Contents/Helpers/rea-tool"]);
|
||||
assert.ok(
|
||||
projection.symlinks.some((path) =>
|
||||
path.endsWith(`${FRAMEWORK}/Versions/Current`),
|
||||
),
|
||||
"Versions/Current symlink was not reported",
|
||||
);
|
||||
anatomies.push(observed);
|
||||
if (format === "directory")
|
||||
await withArtifactMcp(app, async (client) => {
|
||||
assert.deepEqual(
|
||||
await artifactMcpResult(client, "project_apple_application_graph", {
|
||||
inventory_evidence: [inventory],
|
||||
}),
|
||||
projection,
|
||||
);
|
||||
});
|
||||
}
|
||||
assert.deepEqual(anatomies[1], anatomies[0], "ZIP anatomy differs");
|
||||
assert.deepEqual(anatomies[2], anatomies[0], "DMG anatomy differs");
|
||||
const { stdout: attached } = await exec("/usr/bin/hdiutil", ["info"]);
|
||||
assert.ok(!attached.includes(dmg), "DMG remained attached after inventory");
|
||||
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
ok: true,
|
||||
mocked: false,
|
||||
cli: true,
|
||||
stdio_mcp: true,
|
||||
containers: containers.map(({ format }) => format),
|
||||
bundles: EXPECTED_BUNDLES.length,
|
||||
dmg_detached: true,
|
||||
})}\n`,
|
||||
);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
@@ -15,6 +15,23 @@ const root = await mkdtemp(join(tmpdir(), "rea-ui-fixture-"));
|
||||
let child;
|
||||
let fixturePid;
|
||||
try {
|
||||
const childRetrievalTest = join(root, "child-retrieval-test");
|
||||
await promisify(execFile)("/usr/bin/xcrun", [
|
||||
"swiftc",
|
||||
fileURLToPath(
|
||||
new URL("../bridge/native/NativeUIChildren.swift", import.meta.url),
|
||||
),
|
||||
fileURLToPath(
|
||||
new URL(
|
||||
"../tests/conformance/native/native-ui-children/main.swift",
|
||||
import.meta.url,
|
||||
),
|
||||
),
|
||||
"-o",
|
||||
childRetrievalTest,
|
||||
]);
|
||||
await promisify(execFile)(childRetrievalTest, []);
|
||||
|
||||
const contents = join(root, "Fixture.app", "Contents");
|
||||
await mkdir(join(contents, "MacOS"), { recursive: true });
|
||||
await writeFile(
|
||||
@@ -82,7 +99,6 @@ try {
|
||||
"capture_native_ui_scenario",
|
||||
{
|
||||
...scope,
|
||||
restore: "leave-as-is",
|
||||
screenshot: false,
|
||||
steps: [
|
||||
{ kind: "click", path: button.path },
|
||||
@@ -111,8 +127,6 @@ try {
|
||||
String(scope.pid),
|
||||
"--window-id",
|
||||
String(scope.window_id),
|
||||
"--restore",
|
||||
"leave-as-is",
|
||||
"--steps",
|
||||
JSON.stringify([
|
||||
{ kind: "click", path: button.path },
|
||||
|
||||
@@ -2,7 +2,7 @@ import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { createEvidence } from "../dist/domain/evidence.js";
|
||||
import { buildManagedPeFixture } from "./lib/managed-pe-fixture.mjs";
|
||||
import { buildManagedPeFixture } from "./fixtures/managed/pe.mjs";
|
||||
import { functionDossier, json, run } from "./lib/verify-package-core.mjs";
|
||||
|
||||
const buildManagedFixtures = async (workspace) => {
|
||||
|
||||
@@ -37,6 +37,7 @@ import { verifyLargeScreenshotE2e } from "./lib/browser-screenshot-e2e.mjs";
|
||||
import { verifyPopupEventCoverage } from "./lib/browser-popup-e2e.mjs";
|
||||
import { verifyBrowserNetworkEvidence } from "./lib/browser-network-e2e.mjs";
|
||||
import { verifyBrowserScriptExport } from "./lib/browser-script-export-e2e.mjs";
|
||||
import { verifyBrowserModules } from "./lib/browser-module-e2e.mjs";
|
||||
import { verifyBrowserDomDestinations } from "./lib/browser-dom-destinations-e2e.mjs";
|
||||
|
||||
const REAL_BROWSER_STARTUP_TIMEOUT_MS = 60_000;
|
||||
@@ -336,6 +337,7 @@ try {
|
||||
cdp_endpoint: endpoint,
|
||||
target_id: target,
|
||||
});
|
||||
const moduleTrace = await verifyBrowserModules(executable);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
verifier_run: await completeVerifierRun(verifierRun),
|
||||
@@ -357,6 +359,7 @@ try {
|
||||
popupEvents,
|
||||
networkEvidence,
|
||||
scriptExport,
|
||||
moduleTrace,
|
||||
domDestinations,
|
||||
browserScenarioCli: true,
|
||||
browserScenarioAttachCleanup: "disconnected-external",
|
||||
|
||||
@@ -4,7 +4,7 @@ import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { PlaywrightElectronActiveProvider } from "../dist/browser/PlaywrightElectronActiveProvider.js";
|
||||
import { electronActiveObservationInputSchema } from "../dist/domain/electronActiveObservation.js";
|
||||
import { electronActiveObservationInputSchema } from "../dist/domain/javascript/electronActiveObservation.js";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
|
||||
const executable = process.env.REA_ELECTRON_EXECUTABLE;
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Check local website references and the sole GitHub Pages publisher."""
|
||||
|
||||
from html.parser import HTMLParser
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
from urllib.parse import unquote, urlsplit
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
class HtmlReferences(HTMLParser):
|
||||
"""Collect document IDs and references without fetching external URLs."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.ids = set()
|
||||
self.references = []
|
||||
self.duplicate_ids = []
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
attributes = dict(attrs)
|
||||
identifier = attributes.get("id")
|
||||
if identifier is not None:
|
||||
if identifier in self.ids:
|
||||
self.duplicate_ids.append(identifier)
|
||||
self.ids.add(identifier)
|
||||
for attribute in ("href", "src"):
|
||||
reference = attributes.get(attribute)
|
||||
if reference is not None:
|
||||
self.references.append(reference)
|
||||
|
||||
|
||||
def check_site(site):
|
||||
"""Check assets, HTML fragment destinations and SVG syntax."""
|
||||
pages = {}
|
||||
errors = []
|
||||
references = 0
|
||||
for path in sorted(site.rglob("*.html")):
|
||||
parser = HtmlReferences()
|
||||
parser.feed(path.read_text(encoding="utf-8"))
|
||||
pages[path] = parser
|
||||
if site / "index.html" not in pages:
|
||||
errors.append("website/public/index.html is missing")
|
||||
|
||||
for path, page in pages.items():
|
||||
label = path.relative_to(site)
|
||||
for identifier in page.duplicate_ids:
|
||||
errors.append(f"{label}: duplicate ID {identifier!r}")
|
||||
for reference in page.references:
|
||||
url = urlsplit(reference)
|
||||
if url.scheme or url.netloc:
|
||||
continue
|
||||
references += 1
|
||||
if url.path.startswith("/"):
|
||||
errors.append(f"{label}: use a relative site reference: {reference}")
|
||||
continue
|
||||
target = (path.parent / unquote(url.path)).resolve() if url.path else path
|
||||
if not target.is_relative_to(site):
|
||||
errors.append(f"{label}: reference leaves the website: {reference}")
|
||||
continue
|
||||
if target.is_dir():
|
||||
target /= "index.html"
|
||||
if not target.is_file():
|
||||
errors.append(f"{label}: missing local target: {reference}")
|
||||
elif target in pages and url.fragment and unquote(url.fragment) not in pages[target].ids:
|
||||
errors.append(f"{label}: missing HTML fragment: {reference}")
|
||||
|
||||
for path in sorted(site.rglob("*.svg")):
|
||||
try:
|
||||
ET.parse(path)
|
||||
except ET.ParseError as error:
|
||||
errors.append(f"{path.relative_to(site)}: invalid SVG XML: {error}")
|
||||
return len(pages), references, errors
|
||||
|
||||
|
||||
def check_publisher(root):
|
||||
"""Detect a second Pages action that could replace the website."""
|
||||
expected = root / ".github/workflows/website-pages.yml"
|
||||
workflows = root / ".github/workflows"
|
||||
deploy_action = re.compile(r"^\s*-?\s*uses:\s*['\"]?actions/deploy-pages@", re.MULTILINE)
|
||||
publishers = [
|
||||
path
|
||||
for path in sorted(workflows.iterdir())
|
||||
if path.suffix in (".yml", ".yaml") and deploy_action.search(path.read_text(encoding="utf-8"))
|
||||
]
|
||||
if publishers != [expected]:
|
||||
actual = ", ".join(str(path.relative_to(root)) for path in publishers) or "none"
|
||||
return [f"Pages must have one publisher, website-pages.yml; found: {actual}"]
|
||||
return []
|
||||
|
||||
|
||||
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_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("website-pages.yml is the sole GitHub Pages publisher.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -6,18 +6,19 @@ import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { parseEvidence } from "../dist/domain/evidence.js";
|
||||
import { hashAndroidFile } from "../dist/android/AndroidTargetSnapshot.js";
|
||||
import { JADX_RELEASE } from "../dist/android/JadxRelease.js";
|
||||
import fixture from "./fixtures/android-apidemos.json" with { type: "json" };
|
||||
import { verifyAndroidCliCancellation } from "./lib/android-cli-cancellation.mjs";
|
||||
import { parseEvidence } from "../../../dist/domain/evidence.js";
|
||||
import { hashAndroidFile } from "../../../dist/android/AndroidTargetSnapshot.js";
|
||||
import { JADX_RELEASE } from "../../../dist/android/JadxRelease.js";
|
||||
import fixture from "../../fixtures/android/apidemos.json" with { type: "json" };
|
||||
import { verifyAndroidCliCancellation } from "./cli-cancellation.mjs";
|
||||
import { verifyAndroidMetadata } from "./metadata.mjs";
|
||||
|
||||
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
const apk = process.env.REA_ANDROID_TEST_APK;
|
||||
const jar = process.env.REA_JADX_MCP_JAR;
|
||||
if (apk === undefined || jar === undefined)
|
||||
throw new Error(
|
||||
"verify:android requires REA_ANDROID_TEST_APK and REA_JADX_MCP_JAR. Run node scripts/fetch-android-fixtures.mjs and select the downloaded files; see docs/android-analysis.md.",
|
||||
"verify:android requires REA_ANDROID_TEST_APK and REA_JADX_MCP_JAR. Run npm run fixtures:android and select the downloaded files; see docs/android-analysis.md.",
|
||||
);
|
||||
await access(apk);
|
||||
await access(jar);
|
||||
@@ -41,6 +42,14 @@ const entrypoint =
|
||||
process.env.REA_ANDROID_ENTRYPOINT ?? resolve(repository, "scripts/rea.mjs");
|
||||
const environment = { ...process.env, REA_JADX_MCP_JAR: resolve(jar) };
|
||||
const path = resolve(apk);
|
||||
await verifyAndroidMetadata({
|
||||
java,
|
||||
jar: resolve(jar),
|
||||
apk: path,
|
||||
repository,
|
||||
execute,
|
||||
bridge: resolve(dirname(entrypoint), "../bridge/android/ReaJadxBridge.java"),
|
||||
});
|
||||
const tasks = [
|
||||
{
|
||||
name: "inspect_android_package",
|
||||
@@ -212,6 +221,8 @@ const report = {
|
||||
original_apk_unchanged: true,
|
||||
apk_executed: false,
|
||||
cli_sigterm_cleanup: true,
|
||||
metadata_without_code_generation: true,
|
||||
stable_overload_selection: true,
|
||||
};
|
||||
if (process.env.REA_ANDROID_REPORT !== undefined)
|
||||
await writeFile(
|
||||
+2
-2
@@ -3,7 +3,7 @@ import { spawn } from "node:child_process";
|
||||
import {
|
||||
cleanupOwnedProcessGroup,
|
||||
readProcessRunId,
|
||||
} from "../../dist/process/ProcessOwnership.js";
|
||||
} from "../../../dist/process/ProcessOwnership.js";
|
||||
|
||||
/** Exercise a real CLI signal while the owned Java engine is still running. */
|
||||
export const verifyAndroidCliCancellation = async ({
|
||||
@@ -47,7 +47,7 @@ export const verifyAndroidCliCancellation = async ({
|
||||
entry !== null &&
|
||||
Number(entry[2]) === child.pid &&
|
||||
entry[4].includes("engine.jar") &&
|
||||
entry[4].includes("-Xmx512m")
|
||||
entry[4].includes("ReaJadxBridge.java")
|
||||
) {
|
||||
group = Number(entry[3]);
|
||||
const runId = await readProcessRunId(Number(entry[1]));
|
||||
@@ -0,0 +1,64 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { delimiter, join, resolve } from "node:path";
|
||||
|
||||
/** Prove cold metadata stays unmaterialized and overload identities survive code generation. */
|
||||
export const verifyAndroidMetadata = async ({
|
||||
java,
|
||||
jar,
|
||||
apk,
|
||||
bridge,
|
||||
repository,
|
||||
execute,
|
||||
}) => {
|
||||
const javac =
|
||||
process.env.JAVA_HOME === undefined
|
||||
? "javac"
|
||||
: resolve(process.env.JAVA_HOME, "bin/javac");
|
||||
try {
|
||||
await execute(javac, ["-version"], { timeout: 10_000 });
|
||||
} catch (cause) {
|
||||
throw new Error(
|
||||
"verify:android metadata requires the full selected JDK, including javac",
|
||||
{ cause },
|
||||
);
|
||||
}
|
||||
const classes = await mkdtemp(join(tmpdir(), "rea-android-metadata-"));
|
||||
try {
|
||||
await execute(
|
||||
javac,
|
||||
[
|
||||
"-classpath",
|
||||
jar,
|
||||
"-d",
|
||||
classes,
|
||||
bridge,
|
||||
resolve(
|
||||
repository,
|
||||
"tests/conformance/android/ReaJadxMetadataRegression.java",
|
||||
),
|
||||
],
|
||||
{ timeout: 30_000 },
|
||||
);
|
||||
const result = await execute(
|
||||
java,
|
||||
[
|
||||
"-classpath",
|
||||
`${classes}${delimiter}${jar}`,
|
||||
"ReaJadxMetadataRegression",
|
||||
apk,
|
||||
],
|
||||
{ timeout: 120_000, maxBuffer: 8 * 1024 * 1024 },
|
||||
);
|
||||
assert.match(
|
||||
result.stdout,
|
||||
/METADATA_REGRESSION_PASS classes=\d+ overloads=3/u,
|
||||
);
|
||||
console.log(
|
||||
"PASS real cold metadata, full generic types and stable overloaded method selection",
|
||||
);
|
||||
} finally {
|
||||
await rm(classes, { recursive: true, force: true });
|
||||
}
|
||||
};
|
||||
@@ -14,12 +14,12 @@ import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { firmwareResultSchemas } from "../dist/domain/firmwareAnalysis.js";
|
||||
import { parseEvidence } from "../dist/domain/evidence.js";
|
||||
import { hashFirmwareFile } from "../dist/firmware/FirmwareFiles.js";
|
||||
import { firmwareResultSchemas } from "../../../dist/domain/firmware/firmwareAnalysis.js";
|
||||
import { parseEvidence } from "../../../dist/domain/evidence.js";
|
||||
import { hashFirmwareFile } from "../../../dist/firmware/FirmwareFiles.js";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
const entrypoint = resolve(
|
||||
process.env.REA_FIRMWARE_TEST_ENTRYPOINT ??
|
||||
join(repository, "scripts/rea.mjs"),
|
||||
+2
-4
@@ -11,7 +11,7 @@ import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
const root = await realpath(await mkdtemp(join(tmpdir(), "rea-inspector-")));
|
||||
const environment = {
|
||||
...process.env,
|
||||
@@ -43,9 +43,7 @@ try {
|
||||
for (const name of names) {
|
||||
const targetPath =
|
||||
name === "node-target.mjs"
|
||||
? await realpath(
|
||||
join(repository, "tests/conformance/runtime-inspector", name),
|
||||
)
|
||||
? await realpath(join(repository, "tests/conformance/inspector", name))
|
||||
: join(root, name);
|
||||
if (name !== "node-target.mjs")
|
||||
await writeFile(targetPath, "setInterval(() => {}, 1000);\n");
|
||||
+9
-6
@@ -3,16 +3,19 @@
|
||||
import { realpath } from "node:fs/promises";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
import { analyzeJavaScriptApplication } from "../dist/application/JavaScriptApplicationService.js";
|
||||
import { analyzeJavaScriptApplication } from "../../../dist/application/javascript/JavaScriptApplicationService.js";
|
||||
import {
|
||||
compareApplicationVersionsEvidence,
|
||||
compareJavaScriptExportShapesEvidence,
|
||||
traceApplicationFeatureEvidence,
|
||||
} from "../dist/application/JavaScriptApplicationWorkflowService.js";
|
||||
import { applicationVersionComparisonResultSchema } from "../dist/domain/javascriptApplicationVersionComparisonSchemas.js";
|
||||
import { applicationFeatureTraceResultSchema } from "../dist/domain/javascriptFeatureTraceSchemas.js";
|
||||
import { javaScriptExportShapeComparisonResultSchema } from "../dist/domain/javascriptExportShapeComparisonSchemas.js";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
} from "../../../dist/application/javascript/JavaScriptApplicationWorkflowService.js";
|
||||
import { applicationVersionComparisonResultSchema } from "../../../dist/domain/javascript/javascriptApplicationVersionComparisonSchemas.js";
|
||||
import { applicationFeatureTraceResultSchema } from "../../../dist/domain/javascript/javascriptFeatureTraceSchemas.js";
|
||||
import { javaScriptExportShapeComparisonResultSchema } from "../../../dist/domain/javascript/javascriptExportShapeComparisonSchemas.js";
|
||||
import {
|
||||
completeVerifierRun,
|
||||
createVerifierRun,
|
||||
} from "../../lib/verifier-run.mjs";
|
||||
|
||||
const verifierRun = createVerifierRun();
|
||||
|
||||
@@ -4,8 +4,11 @@ import assert from "node:assert/strict";
|
||||
import { constants } from "node:buffer";
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import { canonicalDigest } from "../dist/domain/comparisonSemantics.js";
|
||||
import { createEvidence, parseEvidence } from "../dist/domain/evidence.js";
|
||||
import { canonicalDigest } from "../../../dist/domain/comparisonSemantics.js";
|
||||
import {
|
||||
createEvidence,
|
||||
parseEvidence,
|
||||
} from "../../../dist/domain/evidence.js";
|
||||
|
||||
// This opt-in check crosses the actual engine string limit; keep it outside
|
||||
// routine source tests. Build first, then run this script with the local Node.
|
||||
@@ -9,10 +9,10 @@ import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { parseEvidence } from "../dist/domain/evidence.js";
|
||||
import { javascriptRecoveryResultSchema } from "../dist/domain/javascriptRecovery.js";
|
||||
import { buildRecoveryFixtures } from "./lib/javascript-recovery-fixtures.mjs";
|
||||
import { mcpTextValue } from "./lib/mcp-verifier-results.mjs";
|
||||
import { parseEvidence } from "../../../dist/domain/evidence.js";
|
||||
import { javascriptRecoveryResultSchema } from "../../../dist/domain/javascript/javascriptRecovery.js";
|
||||
import { buildRecoveryFixtures } from "../../fixtures/javascript-recovery/build.mjs";
|
||||
import { mcpTextValue } from "../../lib/mcp-verifier-results.mjs";
|
||||
|
||||
const OFFICIAL_BINARY_SHA256 =
|
||||
"e8c7ca052974604197389f6ace7464e4dd24dce64e2730971521bd6606ec9c9e";
|
||||
@@ -21,7 +21,7 @@ if (!process.env.REA_WAKARU_COMMAND)
|
||||
"Provide REA_WAKARU_COMMAND for the official Wakaru 1.13.0 Linux x64 binary",
|
||||
);
|
||||
const entrypoint =
|
||||
process.argv[2] ?? fileURLToPath(new URL("./rea.mjs", import.meta.url));
|
||||
process.argv[2] ?? fileURLToPath(new URL("../../rea.mjs", import.meta.url));
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-real-recovery-"));
|
||||
const environment = { ...process.env, REA_LOG_LEVEL: "silent" };
|
||||
const transport = new StdioClientTransport({
|
||||
+6
-3
@@ -3,9 +3,12 @@
|
||||
import { readFile, realpath } from "node:fs/promises";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
import { analyzeJavaScriptApplication } from "../dist/application/JavaScriptApplicationService.js";
|
||||
import { reconcileJavaScriptRuntimeEvidence } from "../dist/application/JavaScriptRuntimeReconciliationService.js";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { analyzeJavaScriptApplication } from "../../../dist/application/javascript/JavaScriptApplicationService.js";
|
||||
import { reconcileJavaScriptRuntimeEvidence } from "../../../dist/application/javascript/JavaScriptRuntimeReconciliationService.js";
|
||||
import {
|
||||
completeVerifierRun,
|
||||
createVerifierRun,
|
||||
} from "../../lib/verifier-run.mjs";
|
||||
|
||||
const verifierRun = createVerifierRun();
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { createEvidence } from "../../dist/domain/evidence.js";
|
||||
import { createEvidence } from "../../../dist/domain/evidence.js";
|
||||
|
||||
const COMPLETION_PROVIDER = {
|
||||
id: "rea-managed-conformance",
|
||||
@@ -3,21 +3,21 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { inspectManagedArtifactBytes } from "../dist/dotnet/ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "../dist/dotnet/ManagedMemberInspector.js";
|
||||
import { inspectManagedNativeBoundariesBytes } from "../dist/dotnet/ManagedNativeBoundaryInspector.js";
|
||||
import { compareManagedMemberPaths } from "../dist/application/ManagedMemberComparisonService.js";
|
||||
import { verifyManagedNativeBoundariesEvidence } from "../dist/application/ManagedNativeVerificationService.js";
|
||||
import { importManagedReconstructionEvidence } from "../dist/application/ManagedReconstructionService.js";
|
||||
import { projectManagedApplicationGraphEvidence } from "../dist/application/ManagedApplicationGraphService.js";
|
||||
import { traceApplicationFeatureEvidence } from "../dist/application/JavaScriptApplicationWorkflowService.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../dist/application/InvestigationProviders.js";
|
||||
import { createEvidence } from "../dist/domain/evidence.js";
|
||||
import { inspectManagedArtifactBytes } from "../../../dist/dotnet/ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "../../../dist/dotnet/ManagedMemberInspector.js";
|
||||
import { inspectManagedNativeBoundariesBytes } from "../../../dist/dotnet/ManagedNativeBoundaryInspector.js";
|
||||
import { compareManagedMemberPaths } from "../../../dist/application/managed/ManagedMemberComparisonService.js";
|
||||
import { verifyManagedNativeBoundariesEvidence } from "../../../dist/application/managed/ManagedNativeVerificationService.js";
|
||||
import { importManagedReconstructionEvidence } from "../../../dist/application/managed/ManagedReconstructionService.js";
|
||||
import { projectManagedApplicationGraphEvidence } from "../../../dist/application/managed/ManagedApplicationGraphService.js";
|
||||
import { traceApplicationFeatureEvidence } from "../../../dist/application/javascript/JavaScriptApplicationWorkflowService.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../../../dist/application/InvestigationProviders.js";
|
||||
import { createEvidence } from "../../../dist/domain/evidence.js";
|
||||
import {
|
||||
alternateMvid,
|
||||
buildNativePeFixture,
|
||||
} from "./lib/managed-pe-fixture.mjs";
|
||||
import { createManagedConformanceSupport } from "./lib/managed-conformance-support.mjs";
|
||||
} from "../../fixtures/managed/pe.mjs";
|
||||
import { createManagedConformanceSupport } from "./support.mjs";
|
||||
import {
|
||||
comparisonLimits,
|
||||
defaultIlBody,
|
||||
@@ -25,9 +25,12 @@ import {
|
||||
inspectionLimits,
|
||||
memberLimits,
|
||||
nativeBoundaryLimits,
|
||||
} from "./lib/managed-conformance-config.mjs";
|
||||
import { createManagedCompletionReport } from "./lib/managed-completion-report.mjs";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
} from "./config.mjs";
|
||||
import { createManagedCompletionReport } from "./completion-report.mjs";
|
||||
import {
|
||||
completeVerifierRun,
|
||||
createVerifierRun,
|
||||
} from "../../lib/verifier-run.mjs";
|
||||
|
||||
const verifierRun = createVerifierRun();
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-managed-conformance-"));
|
||||
@@ -1,18 +1,18 @@
|
||||
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { parseBinaryTarget } from "../../dist/application/BinaryTargetResolver.js";
|
||||
import { inspectManagedArtifactBytes } from "../../dist/dotnet/ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "../../dist/dotnet/ManagedMemberInspector.js";
|
||||
import { projectManagedApplicationGraphEvidence } from "../../dist/application/ManagedApplicationGraphService.js";
|
||||
import { traceApplicationFeatureEvidence } from "../../dist/application/JavaScriptApplicationWorkflowService.js";
|
||||
import { createEvidence } from "../../dist/domain/evidence.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../../dist/application/InvestigationProviders.js";
|
||||
import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js";
|
||||
import { inspectManagedArtifactBytes } from "../../../dist/dotnet/ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "../../../dist/dotnet/ManagedMemberInspector.js";
|
||||
import { projectManagedApplicationGraphEvidence } from "../../../dist/application/managed/ManagedApplicationGraphService.js";
|
||||
import { traceApplicationFeatureEvidence } from "../../../dist/application/javascript/JavaScriptApplicationWorkflowService.js";
|
||||
import { createEvidence } from "../../../dist/domain/evidence.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../../../dist/application/InvestigationProviders.js";
|
||||
import {
|
||||
ensure,
|
||||
methodTokenRow,
|
||||
parseManagedAppManifest,
|
||||
} from "./managed-conformance-manifest-parser.mjs";
|
||||
} from "./manifest-parser.mjs";
|
||||
|
||||
export const createManagedManifestVerifier = (context) => ({
|
||||
verifyManagedAppManifest: (rawManifest, manifestPath) =>
|
||||
@@ -5,12 +5,12 @@ import { basename, isAbsolute, join, resolve } from "node:path";
|
||||
import assert from "node:assert/strict";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { inspectManagedArtifactBytes } from "../../dist/dotnet/ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "../../dist/dotnet/ManagedMemberInspector.js";
|
||||
import { importManagedReconstructionEvidence } from "../../dist/application/ManagedReconstructionService.js";
|
||||
import { createEvidence } from "../../dist/domain/evidence.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../../dist/application/InvestigationProviders.js";
|
||||
import { createManagedManifestVerifier } from "./managed-conformance-manifest.mjs";
|
||||
import { inspectManagedArtifactBytes } from "../../../dist/dotnet/ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "../../../dist/dotnet/ManagedMemberInspector.js";
|
||||
import { importManagedReconstructionEvidence } from "../../../dist/application/managed/ManagedReconstructionService.js";
|
||||
import { createEvidence } from "../../../dist/domain/evidence.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../../../dist/application/InvestigationProviders.js";
|
||||
import { createManagedManifestVerifier } from "./manifest.mjs";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
@@ -2,9 +2,9 @@ import { createHash } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { parseBinaryTarget } from "../../dist/application/BinaryTargetResolver.js";
|
||||
import { buildManagedPeFixture } from "./managed-pe-fixture.mjs";
|
||||
import { createManagedConformanceOracleSupport } from "./managed-conformance-oracles.mjs";
|
||||
import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js";
|
||||
import { buildManagedPeFixture } from "../../fixtures/managed/pe.mjs";
|
||||
import { createManagedConformanceOracleSupport } from "./oracles.mjs";
|
||||
|
||||
export const createManagedConformanceSupport = (context) => {
|
||||
const fixtureBytes = async (name, bytes) => {
|
||||
@@ -2,9 +2,9 @@
|
||||
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: "26"
|
||||
tool_count: 129
|
||||
catalog_digest: "856ce0f99cc6e858f3317e40cec27c2cfd479ae7ff6a8d9cf695504d6166b64d"
|
||||
version: "27"
|
||||
tool_count: 131
|
||||
catalog_digest: "54ac160db3b96580a9a0203a07fe3db01ffc05224db621f48b4abdf979e4f50b"
|
||||
---
|
||||
|
||||
# REA
|
||||
|
||||
@@ -69,6 +69,14 @@ ASAR, or DMG inputs when the artifact graph and findings help answer the
|
||||
question. It returns the complete artifact graph inline. Cite graph manifest
|
||||
IDs when using them.
|
||||
|
||||
For an IPA or a macOS `.app`, ZIP, or DMG, pass the inventory Evidence to
|
||||
`project_apple_application_graph`. It reports application roots and nested
|
||||
bundles by path convention: app extensions, XPC services, frameworks, login
|
||||
items, system and driver extensions, and plug-ins. It also lists privileged
|
||||
helpers, launchd plists, symlinks, and each bundle's `info_plist_path` and
|
||||
executable candidates. Roles are path conventions, not parsed plists; read the
|
||||
listed plists with `inspect_plist`.
|
||||
|
||||
Use `extract_artifact` when materialized files are needed. It takes no arguments
|
||||
and materializes all regular files into a fresh temporary directory chosen by
|
||||
REA. Symlinks and encrypted entries are inventory facts, not extractable files.
|
||||
|
||||
@@ -2,8 +2,11 @@ import { constants, createReadStream } from "node:fs";
|
||||
import { chmod, copyFile } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { join } from "node:path";
|
||||
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
|
||||
import type { AndroidOperation } from "../domain/androidAnalysis.js";
|
||||
import {
|
||||
AnalysisInputError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import type { AndroidOperation } from "../domain/android/androidAnalysis.js";
|
||||
|
||||
/** Stream a local file digest without retaining the APK or JAR in memory. */
|
||||
export const hashAndroidFile = async (path: string): Promise<string> => {
|
||||
@@ -37,10 +40,18 @@ export const snapshotAndroidTarget = async (
|
||||
/** Fingerprint the actual immutable engine bytes executed for this observation. */
|
||||
export const snapshotAndroidEngine = async (
|
||||
source: string,
|
||||
sha256: string,
|
||||
root: string,
|
||||
operation: AndroidOperation,
|
||||
): Promise<{ path: string; sha256: string }> => {
|
||||
const path = join(root, "engine.jar");
|
||||
await copyFile(source, path, constants.COPYFILE_EXCL);
|
||||
await chmod(path, 0o400);
|
||||
return { path, sha256: await hashAndroidFile(path) };
|
||||
if ((await hashAndroidFile(path)) !== sha256)
|
||||
throw new AnalysisCapabilityUnavailableError(
|
||||
"jadx",
|
||||
operation,
|
||||
"REA_JADX_MCP_JAR bytes changed during admission; retry with a stable engine file.",
|
||||
);
|
||||
return { path, sha256 };
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { z } from "zod";
|
||||
import type { AndroidRequest } from "../domain/androidAnalysis.js";
|
||||
import { androidResultSchemas } from "../domain/androidAnalysis.js";
|
||||
import type { AndroidRequest } from "../domain/android/androidAnalysis.js";
|
||||
import { androidResultSchemas } from "../domain/android/androidAnalysis.js";
|
||||
import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisInputError,
|
||||
@@ -137,19 +137,20 @@ const searchClasses = async (
|
||||
engine: Engine,
|
||||
): Promise<JsonValue> => {
|
||||
const classes: string[] = [];
|
||||
const pageSize = 4096;
|
||||
let total: number | undefined;
|
||||
do {
|
||||
const page = jadxClassPageSchema.parse(
|
||||
await tools.json("list_classes", {
|
||||
offset: classes.length,
|
||||
limit: 200,
|
||||
limit: pageSize,
|
||||
}),
|
||||
);
|
||||
if (
|
||||
(total !== undefined && total !== page.total) ||
|
||||
page.offset !== classes.length ||
|
||||
page.limit !== 200 ||
|
||||
page.items.length > 200
|
||||
page.limit !== pageSize ||
|
||||
page.items.length > pageSize
|
||||
)
|
||||
throw new AnalysisOutputError(
|
||||
request.operation,
|
||||
@@ -307,7 +308,7 @@ const traceReferences = async (
|
||||
throw new AnalysisCapabilityUnavailableError(
|
||||
"jadx",
|
||||
request.operation,
|
||||
`JADX ${JADX_RELEASE.version} references select the first same-name method; ${summary.full_name}.${methodName} has ${candidates.length} overloads. No unambiguous reference result is available.`,
|
||||
`References require a unique method name; ${summary.full_name}.${methodName} has ${candidates.length} overloads. No unambiguous reference result is available.`,
|
||||
);
|
||||
const expectedTarget =
|
||||
methodName === undefined
|
||||
|
||||
@@ -2,13 +2,14 @@ import { access, realpath, stat } from "node:fs/promises";
|
||||
import { constants } from "node:fs";
|
||||
import { isAbsolute, join } from "node:path";
|
||||
import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js";
|
||||
import type { AndroidOperation } from "../domain/androidAnalysis.js";
|
||||
import type { AndroidOperation } from "../domain/android/androidAnalysis.js";
|
||||
import { JADX_JAR_CONFIGURATION_REMEDIATION } from "./JadxRelease.js";
|
||||
|
||||
/** Caller-supplied tools, resolved only when an Android operation is selected. */
|
||||
export interface JadxConfiguration {
|
||||
readonly jar: string;
|
||||
readonly java: string;
|
||||
readonly jvmArguments: readonly string[];
|
||||
}
|
||||
|
||||
/** Admit explicit local tooling without installing or changing host configuration. */
|
||||
@@ -20,7 +21,7 @@ export const resolveJadxConfiguration = async (
|
||||
new AnalysisCapabilityUnavailableError("jadx", operation, reason);
|
||||
if (process.platform !== "linux" && process.platform !== "darwin")
|
||||
throw unavailable(
|
||||
`Android JADX subprocess ownership is unsupported on ${process.platform}; use Linux or macOS. Only Linux has real-provider verification.`,
|
||||
`Android JADX subprocess ownership is unsupported on ${process.platform}; use Linux or macOS. The metadata bridge has real-provider verification on macOS arm64.`,
|
||||
);
|
||||
const jar = environment.REA_JADX_MCP_JAR;
|
||||
if (environment.JAVA_HOME !== undefined && !isAbsolute(environment.JAVA_HOME))
|
||||
@@ -55,5 +56,25 @@ export const resolveJadxConfiguration = async (
|
||||
);
|
||||
}
|
||||
}
|
||||
return { jar: await realpath(jar), java };
|
||||
const jvmArguments: string[] = [];
|
||||
for (const [name, prefix, suffix] of [
|
||||
["REA_JADX_HEAP_MIB", "-Xmx", "m"],
|
||||
["REA_JADX_ACTIVE_PROCESSOR_COUNT", "-XX:ActiveProcessorCount=", ""],
|
||||
] as const) {
|
||||
const value = environment[name];
|
||||
if (value === undefined) continue;
|
||||
if (!/^[1-9]\d*$/u.test(value) || !Number.isSafeInteger(Number(value)))
|
||||
throw unavailable(
|
||||
`${name} must be a positive safe integer; received ${value}.`,
|
||||
);
|
||||
if (
|
||||
name === "REA_JADX_ACTIVE_PROCESSOR_COUNT" &&
|
||||
Number(value) > 2_147_483_647
|
||||
)
|
||||
throw unavailable(
|
||||
`${name} exceeds the JVM's signed 32-bit processor-count range.`,
|
||||
);
|
||||
jvmArguments.push(`${prefix}${value}${suffix}`);
|
||||
}
|
||||
return { jar: await realpath(jar), java, jvmArguments };
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ import { cleanupOwnedProcessGroup } from "../process/ProcessOwnership.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
|
||||
const MAX_FRAME_BYTES = 8 * 1024 * 1024;
|
||||
const MAX_SESSION_BYTES = 32 * 1024 * 1024;
|
||||
const MAX_OPERATION_BYTES = 32 * 1024 * 1024;
|
||||
|
||||
/** Injectable process acquisition seam; the production launcher owns a POSIX group. */
|
||||
export type JadxLauncher = (
|
||||
@@ -32,7 +32,8 @@ export class JadxMcpTransport implements Transport {
|
||||
readonly runId = randomUUID();
|
||||
#spawned: SpawnedOwnedProviderProcess | undefined;
|
||||
#supervisor: ProviderProcessSupervisor | undefined;
|
||||
#buffer = Buffer.alloc(0);
|
||||
#frameBuffer = Buffer.alloc(0);
|
||||
#frameLength = 0;
|
||||
#bytes = 0;
|
||||
#started = false;
|
||||
#closed = false;
|
||||
@@ -61,12 +62,16 @@ export class JadxMcpTransport implements Transport {
|
||||
stdin: "pipe",
|
||||
});
|
||||
this.#spawned = spawned;
|
||||
this.#supervisor = new ProviderProcessSupervisor({
|
||||
...spawned,
|
||||
ownsProcessLifetime: true,
|
||||
cleanup:
|
||||
spawned.cleanup ?? (() => cleanupOwnedProcessGroup(spawned.ownership)),
|
||||
});
|
||||
this.#supervisor = new ProviderProcessSupervisor(
|
||||
{
|
||||
...spawned,
|
||||
ownsProcessLifetime: true,
|
||||
cleanup:
|
||||
spawned.cleanup ??
|
||||
(() => cleanupOwnedProcessGroup(spawned.ownership)),
|
||||
},
|
||||
{ captureStdout: false },
|
||||
);
|
||||
if (
|
||||
spawned.process.stdin === undefined ||
|
||||
spawned.process.stdin === null ||
|
||||
@@ -109,8 +114,16 @@ export class JadxMcpTransport implements Transport {
|
||||
return this.#failure;
|
||||
}
|
||||
|
||||
/** Scope both the byte budget and diagnostic retention to the next serialized request. */
|
||||
beginOperation(): void {
|
||||
this.#bytes = 0;
|
||||
this.#supervisor?.resetOutput();
|
||||
}
|
||||
|
||||
async #stop(): Promise<void> {
|
||||
this.#closed = true;
|
||||
this.#frameBuffer = Buffer.alloc(0);
|
||||
this.#frameLength = 0;
|
||||
// A cancellation may race acquisition. Join it before inspecting ownership.
|
||||
await this.#starting?.catch(() => undefined);
|
||||
const process = this.#spawned?.process;
|
||||
@@ -136,10 +149,10 @@ export class JadxMcpTransport implements Transport {
|
||||
};
|
||||
readonly #countStderr = (chunk: Buffer | string): void => {
|
||||
this.#bytes += Buffer.byteLength(chunk);
|
||||
if (this.#bytes > MAX_SESSION_BYTES)
|
||||
if (this.#bytes > MAX_OPERATION_BYTES)
|
||||
this.#fail(
|
||||
new Error(
|
||||
`JADX session output exceeds ${MAX_SESSION_BYTES} bytes; no complete result is available`,
|
||||
`JADX operation output exceeds ${MAX_OPERATION_BYTES} bytes; no complete result is available`,
|
||||
),
|
||||
);
|
||||
};
|
||||
@@ -147,30 +160,50 @@ export class JadxMcpTransport implements Transport {
|
||||
if (this.#closed) return;
|
||||
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||
this.#bytes += bytes.length;
|
||||
if (this.#bytes > MAX_SESSION_BYTES) {
|
||||
if (this.#bytes > MAX_OPERATION_BYTES) {
|
||||
this.#fail(
|
||||
new Error(
|
||||
`JADX session output exceeds ${MAX_SESSION_BYTES} bytes; no complete result is available`,
|
||||
`JADX operation output exceeds ${MAX_OPERATION_BYTES} bytes; no complete result is available`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.#buffer = Buffer.concat([this.#buffer, bytes]);
|
||||
try {
|
||||
let newline: number;
|
||||
while ((newline = this.#buffer.indexOf(10)) !== -1) {
|
||||
if (newline > MAX_FRAME_BYTES)
|
||||
let offset = 0;
|
||||
while (offset < bytes.length) {
|
||||
const newline = bytes.indexOf(10, offset);
|
||||
const end = newline === -1 ? bytes.length : newline;
|
||||
const fragment = bytes.subarray(offset, end);
|
||||
const frameBytes = this.#frameLength + fragment.length;
|
||||
if (frameBytes > MAX_FRAME_BYTES)
|
||||
throw new Error(`JADX MCP frame exceeds ${MAX_FRAME_BYTES} bytes`);
|
||||
const line = this.#buffer.subarray(0, newline).toString("utf8");
|
||||
this.#buffer = this.#buffer.subarray(newline + 1);
|
||||
this.#appendFrame(fragment, frameBytes);
|
||||
if (newline === -1) return;
|
||||
const line = this.#frameBuffer
|
||||
.subarray(0, this.#frameLength)
|
||||
.toString("utf8");
|
||||
this.#frameBuffer = Buffer.alloc(0);
|
||||
this.#frameLength = 0;
|
||||
this.onmessage?.(deserializeMessage(line));
|
||||
offset = newline + 1;
|
||||
}
|
||||
if (this.#buffer.length > MAX_FRAME_BYTES)
|
||||
throw new Error(`JADX MCP frame exceeds ${MAX_FRAME_BYTES} bytes`);
|
||||
} catch (cause) {
|
||||
this.#fail(cause instanceof Error ? cause : new Error(String(cause)));
|
||||
}
|
||||
};
|
||||
#appendFrame(fragment: Buffer, frameBytes: number): void {
|
||||
if (frameBytes > this.#frameBuffer.length) {
|
||||
const capacity = Math.min(
|
||||
MAX_FRAME_BYTES,
|
||||
Math.max(frameBytes, this.#frameBuffer.length * 2, 1),
|
||||
);
|
||||
const grown = Buffer.allocUnsafe(capacity);
|
||||
this.#frameBuffer.copy(grown, 0, 0, this.#frameLength);
|
||||
this.#frameBuffer = grown;
|
||||
}
|
||||
fragment.copy(this.#frameBuffer, this.#frameLength);
|
||||
this.#frameLength = frameBytes;
|
||||
}
|
||||
#fail(error: Error): void {
|
||||
this.#failure ??= error.message;
|
||||
this.onerror?.(error);
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { z } from "zod";
|
||||
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import type { AndroidOperation } from "../domain/androidAnalysis.js";
|
||||
import type { AndroidOperation } from "../domain/android/androidAnalysis.js";
|
||||
|
||||
const count = z.number().int().nonnegative();
|
||||
/** Values observed inside the exact JVM producing the analysis. */
|
||||
export const jadxRuntimeSchema = z.object({
|
||||
engine_reported_version: z.string().min(1),
|
||||
max_heap_bytes: z.number().int().positive().safe(),
|
||||
available_processors: z.number().int().positive(),
|
||||
java_version: z.string().min(1),
|
||||
metadata_scope: z.literal("parsed_members"),
|
||||
});
|
||||
export const jadxLoadSchema = z.object({
|
||||
state: z.literal("LOADED"),
|
||||
apk_path: z.string(),
|
||||
|
||||
+159
-43
@@ -1,29 +1,37 @@
|
||||
import { z } from "zod";
|
||||
import type { AndroidAnalysisPort } from "../application/AndroidAnalysisPort.js";
|
||||
import { chmod, copyFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { AndroidAnalysisPort } from "../application/android/AndroidAnalysisPort.js";
|
||||
import type { ExecutionOptions } from "../application/AnalysisProvider.js";
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisOutputError,
|
||||
AnalysisInputError,
|
||||
AnalysisTimeoutError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import type { AndroidRequest } from "../domain/androidAnalysis.js";
|
||||
import type { AndroidRequest } from "../domain/android/androidAnalysis.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTarget.js";
|
||||
import { err, ok } from "../domain/result.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
|
||||
import {
|
||||
snapshotAndroidEngine,
|
||||
hashAndroidFile,
|
||||
snapshotAndroidTarget,
|
||||
} from "./AndroidTargetSnapshot.js";
|
||||
import { resolveJadxConfiguration } from "./JadxConfiguration.js";
|
||||
import type { JadxLauncher } from "./JadxMcpTransport.js";
|
||||
import { JADX_HEAP_XMX_ARGUMENT } from "./JadxRelease.js";
|
||||
|
||||
import { JadxSession } from "./JadxSession.js";
|
||||
|
||||
const OPERATION_TIMEOUT_MS = 120_000;
|
||||
const BRIDGE_SOURCE = fileURLToPath(
|
||||
new URL("../../bridge/android/ReaJadxBridge.java", import.meta.url),
|
||||
);
|
||||
type Outcome = Awaited<ReturnType<AndroidAnalysisPort["execute"]>>;
|
||||
|
||||
const waitForTurn = (
|
||||
@@ -74,11 +82,11 @@ const executionError = (context: {
|
||||
});
|
||||
};
|
||||
|
||||
const cleanup = async (
|
||||
const cleanup = async <Value>(
|
||||
session: JadxSession | undefined,
|
||||
root: PrivateRuntimeRoot | undefined,
|
||||
previous: Outcome,
|
||||
): Promise<Outcome> => {
|
||||
previous: Result<Value, AnalysisError>,
|
||||
): Promise<Result<Value, AnalysisError>> => {
|
||||
const diagnostics = {
|
||||
previous_error: previous.ok
|
||||
? null
|
||||
@@ -125,10 +133,23 @@ const cleanup = async (
|
||||
return previous;
|
||||
};
|
||||
|
||||
/** One queued engine per REA instance, with a fresh owned workspace for every request. */
|
||||
interface RetainedSession {
|
||||
readonly key: string;
|
||||
readonly root: PrivateRuntimeRoot;
|
||||
readonly session: JadxSession;
|
||||
readonly snapshot: string;
|
||||
readonly jarHash: string;
|
||||
readonly bridgeHash: string;
|
||||
}
|
||||
|
||||
/** One serialized, immutable APK session per REA instance, retired on idle or shutdown. */
|
||||
export class JadxProvider implements AndroidAnalysisPort {
|
||||
#tail: Promise<void> = Promise.resolve();
|
||||
#cleanupFailure: AnalysisError | undefined;
|
||||
#retained: RetainedSession | undefined;
|
||||
#idle: NodeJS.Timeout | undefined;
|
||||
readonly #shutdown = new AbortController();
|
||||
#closePromise: Promise<void> | undefined;
|
||||
constructor(
|
||||
readonly environment: Readonly<
|
||||
Record<string, string | undefined>
|
||||
@@ -142,30 +163,69 @@ export class JadxProvider implements AndroidAnalysisPort {
|
||||
request: AndroidRequest,
|
||||
options?: ExecutionOptions,
|
||||
): Promise<Outcome> {
|
||||
if (this.#shutdown.signal.aborted)
|
||||
return err(new AnalysisCancelledError(request.operation));
|
||||
const signal =
|
||||
options?.signal === undefined
|
||||
? this.#shutdown.signal
|
||||
: AbortSignal.any([options.signal, this.#shutdown.signal]);
|
||||
const predecessor = this.#tail;
|
||||
let release: () => void = () => {};
|
||||
this.#tail = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
try {
|
||||
await waitForTurn(predecessor, request, options?.signal);
|
||||
await waitForTurn(predecessor, request, signal);
|
||||
} catch {
|
||||
void predecessor.then(release);
|
||||
return err(new AnalysisCancelledError(request.operation));
|
||||
}
|
||||
try {
|
||||
if (options?.signal?.aborted === true)
|
||||
if (signal.aborted)
|
||||
return err(new AnalysisCancelledError(request.operation));
|
||||
if (this.#cleanupFailure !== undefined) return err(this.#cleanupFailure);
|
||||
const outcome = await this.#execute(target, request, options);
|
||||
clearTimeout(this.#idle);
|
||||
const outcome = await this.#execute(target, request, { signal });
|
||||
if (!outcome.ok && outcome.error.cleanupIncomplete)
|
||||
this.#cleanupFailure = outcome.error;
|
||||
if (outcome.ok) this.#scheduleIdle();
|
||||
return outcome;
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
/** Cancel queued/active operations and join the owned engine and workspace cleanup. */
|
||||
close(): Promise<void> {
|
||||
this.#shutdown.abort();
|
||||
clearTimeout(this.#idle);
|
||||
this.#closePromise ??= this.#tail.then(() => this.#retire());
|
||||
return this.#closePromise;
|
||||
}
|
||||
|
||||
async #retire(): Promise<void> {
|
||||
if (this.#cleanupFailure !== undefined) throw this.#cleanupFailure;
|
||||
const retained = this.#retained;
|
||||
this.#retained = undefined;
|
||||
const result = await cleanup(
|
||||
retained?.session,
|
||||
retained?.root,
|
||||
ok(undefined),
|
||||
);
|
||||
if (!result.ok) {
|
||||
this.#cleanupFailure = result.error;
|
||||
throw result.error;
|
||||
}
|
||||
}
|
||||
|
||||
#scheduleIdle(): void {
|
||||
this.#idle = setTimeout(() => {
|
||||
const retirement = this.#tail.then(() => this.#retire());
|
||||
this.#tail = retirement.catch(() => undefined);
|
||||
}, 60_000);
|
||||
this.#idle.unref();
|
||||
}
|
||||
|
||||
async #execute(
|
||||
target: BinaryTarget,
|
||||
request: AndroidRequest,
|
||||
@@ -191,47 +251,99 @@ export class JadxProvider implements AndroidAnalysisPort {
|
||||
request.operation,
|
||||
);
|
||||
signal.throwIfAborted();
|
||||
root = await PrivateRuntimeRoot.create({ prefix: "rea-android-" });
|
||||
const engine = await snapshotAndroidEngine(configuration.jar, root.path);
|
||||
const snapshot = await snapshotAndroidTarget(
|
||||
target.path,
|
||||
target.sha256,
|
||||
root.path,
|
||||
request.operation,
|
||||
);
|
||||
signal.throwIfAborted();
|
||||
session = new JadxSession(
|
||||
{
|
||||
command: configuration.java,
|
||||
arguments: [
|
||||
JADX_HEAP_XMX_ARGUMENT,
|
||||
"-XX:ActiveProcessorCount=1",
|
||||
"-jar",
|
||||
engine.path,
|
||||
"--threads",
|
||||
"1",
|
||||
"--max-source-bytes",
|
||||
"1048576",
|
||||
"--decompile-timeout-ms",
|
||||
"90000",
|
||||
],
|
||||
cwd: root.path,
|
||||
hostEnvironment: this.environment,
|
||||
env: {
|
||||
_JAVA_OPTIONS: `${JADX_HEAP_XMX_ARGUMENT} -XX:ActiveProcessorCount=1`,
|
||||
const jarHash = await hashAndroidFile(configuration.jar);
|
||||
const bridgeHash = await hashAndroidFile(BRIDGE_SOURCE);
|
||||
if ((await hashAndroidFile(target.path)) !== target.sha256)
|
||||
throw new AnalysisInputError(request.operation, undefined, [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_value",
|
||||
message:
|
||||
"APK bytes changed after admission; retry against a stable file.",
|
||||
},
|
||||
},
|
||||
this.launcher,
|
||||
);
|
||||
]);
|
||||
const key = JSON.stringify({
|
||||
path: target.path,
|
||||
sha256: target.sha256,
|
||||
jar: configuration.jar,
|
||||
jarHash,
|
||||
bridgeHash,
|
||||
java: configuration.java,
|
||||
arguments: configuration.jvmArguments,
|
||||
options: [
|
||||
this.environment.JAVA_TOOL_OPTIONS,
|
||||
this.environment._JAVA_OPTIONS,
|
||||
this.environment.JDK_JAVA_OPTIONS,
|
||||
this.environment.PATH,
|
||||
this.environment.JAVA_HOME,
|
||||
],
|
||||
});
|
||||
if (this.#retained !== undefined && this.#retained.key !== key)
|
||||
await this.#retire();
|
||||
let retained = this.#retained;
|
||||
if (retained === undefined) {
|
||||
root = await PrivateRuntimeRoot.create({ prefix: "rea-android-" });
|
||||
const engine = await snapshotAndroidEngine(
|
||||
configuration.jar,
|
||||
jarHash,
|
||||
root.path,
|
||||
request.operation,
|
||||
);
|
||||
const snapshot = await snapshotAndroidTarget(
|
||||
target.path,
|
||||
target.sha256,
|
||||
root.path,
|
||||
request.operation,
|
||||
);
|
||||
const bridge = join(root.path, "ReaJadxBridge.java");
|
||||
await copyFile(BRIDGE_SOURCE, bridge);
|
||||
await chmod(bridge, 0o400);
|
||||
if ((await hashAndroidFile(bridge)) !== bridgeHash)
|
||||
throw new AnalysisCapabilityUnavailableError(
|
||||
"jadx",
|
||||
request.operation,
|
||||
"REA Android bridge bytes changed during admission; retry with a stable installation.",
|
||||
);
|
||||
signal.throwIfAborted();
|
||||
session = new JadxSession(
|
||||
{
|
||||
command: configuration.java,
|
||||
arguments: [
|
||||
...configuration.jvmArguments,
|
||||
"--class-path",
|
||||
engine.path,
|
||||
bridge,
|
||||
],
|
||||
cwd: root.path,
|
||||
hostEnvironment: this.environment,
|
||||
},
|
||||
this.launcher,
|
||||
);
|
||||
retained = {
|
||||
key,
|
||||
root,
|
||||
session,
|
||||
snapshot,
|
||||
jarHash: engine.sha256,
|
||||
bridgeHash,
|
||||
};
|
||||
}
|
||||
root = retained.root;
|
||||
session = retained.session;
|
||||
const { snapshot } = retained;
|
||||
outcome = ok(
|
||||
await session.execute({
|
||||
request,
|
||||
target,
|
||||
snapshot,
|
||||
jarHash: engine.sha256,
|
||||
jarHash: retained.jarHash,
|
||||
bridgeHash: retained.bridgeHash,
|
||||
signal,
|
||||
}),
|
||||
);
|
||||
signal.throwIfAborted();
|
||||
this.#retained = retained;
|
||||
return outcome;
|
||||
} catch (cause) {
|
||||
outcome = err(
|
||||
executionError({
|
||||
@@ -243,6 +355,10 @@ export class JadxProvider implements AndroidAnalysisPort {
|
||||
}),
|
||||
);
|
||||
}
|
||||
// An existing session also needs retirement when admission fails before selection.
|
||||
root ??= this.#retained?.root;
|
||||
session ??= this.#retained?.session;
|
||||
this.#retained = undefined;
|
||||
return cleanup(session, root, outcome);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ export const JADX_RELEASE = Object.freeze({
|
||||
url: "https://github.com/1013503897/jadx-headless-mcp/releases/download/v0.7.1/jadx-headless-mcp-0.7.1-all.jar",
|
||||
});
|
||||
|
||||
/** JVM heap ceiling applied to every owned JADX worker process. */
|
||||
export const JADX_HEAP_LIMIT_MIB = 512;
|
||||
|
||||
/** JVM `-Xmx` argument matching {@link JADX_HEAP_LIMIT_MIB}. */
|
||||
export const JADX_HEAP_XMX_ARGUMENT = `-Xmx${JADX_HEAP_LIMIT_MIB}m`;
|
||||
/** Identity of REA's metadata bridge, distinct from the producing engine. */
|
||||
export const JADX_BRIDGE_IDENTITY = Object.freeze({
|
||||
name: "rea-jadx-bridge",
|
||||
version: "1",
|
||||
});
|
||||
|
||||
/** Remediation when `REA_JADX_MCP_JAR` is missing or not an absolute path. */
|
||||
export const JADX_JAR_CONFIGURATION_REMEDIATION = `Set REA_JADX_MCP_JAR to the absolute path of a caller-supplied jadx-headless-mcp ${JADX_RELEASE.version} JAR. REA does not download or install it.`;
|
||||
@@ -26,5 +26,6 @@ export const JADX_PROVIDER_IDENTITY = Object.freeze({
|
||||
export const JADX_LIMITATIONS = [
|
||||
"Static decompiler observations do not establish runtime behavior; the APK is never executed.",
|
||||
"Method signatures use provider display types; exact DEX descriptors and instruction offsets are unavailable. Overload indices are local to this engine and artifact.",
|
||||
"Class inventories use parsed metadata before code generation, including synthetic members that may be omitted from decompiled source. Source positions remain unknown.",
|
||||
"Manifest summary fields are extracted by the upstream engine; decoded XML is retained for verification. APK signatures, split APKs, native libraries and Android runtime capture are not verified by this provider.",
|
||||
] as const;
|
||||
|
||||
+43
-18
@@ -3,7 +3,7 @@ import {
|
||||
createAnalysisExecution,
|
||||
type AnalysisExecution,
|
||||
} from "../application/AnalysisProvider.js";
|
||||
import type { AndroidRequest } from "../domain/androidAnalysis.js";
|
||||
import type { AndroidRequest } from "../domain/android/androidAnalysis.js";
|
||||
import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisOutputError,
|
||||
@@ -16,11 +16,12 @@ import { analyzeJadxRequest, type JadxToolPort } from "./JadxAnalysis.js";
|
||||
import { JadxMcpTransport, type JadxLauncher } from "./JadxMcpTransport.js";
|
||||
import {
|
||||
jadxLoadSchema,
|
||||
jadxRuntimeSchema,
|
||||
parseJadxEnvelope,
|
||||
parseJadxJson,
|
||||
} from "./JadxProtocol.js";
|
||||
import {
|
||||
JADX_HEAP_LIMIT_MIB,
|
||||
JADX_BRIDGE_IDENTITY,
|
||||
JADX_RELEASE,
|
||||
JADX_PROVIDER_IDENTITY,
|
||||
JADX_LIMITATIONS,
|
||||
@@ -30,7 +31,7 @@ import {
|
||||
export class JadxSession {
|
||||
readonly transport: JadxMcpTransport;
|
||||
readonly #client = new Client({ name: "rea-android-adapter", version: "1" });
|
||||
readonly #raw: JsonValue[] = [];
|
||||
#loaded: ReturnType<typeof jadxLoadSchema.parse> | undefined;
|
||||
|
||||
constructor(
|
||||
options: Omit<OwnedProviderProcessSpawnOptions, "runId" | "stdin">,
|
||||
@@ -45,6 +46,7 @@ export class JadxSession {
|
||||
readonly target: BinaryTarget;
|
||||
readonly snapshot: string;
|
||||
readonly jarHash: string;
|
||||
readonly bridgeHash: string;
|
||||
readonly signal: AbortSignal;
|
||||
}): Promise<AnalysisExecution> {
|
||||
const { request, target, snapshot, jarHash, signal } = context;
|
||||
@@ -54,42 +56,64 @@ export class JadxSession {
|
||||
signal.addEventListener("abort", abort, { once: true });
|
||||
try {
|
||||
signal.throwIfAborted();
|
||||
await this.#client.connect(this.transport, { timeout: 30_000 });
|
||||
this.transport.beginOperation();
|
||||
const raw: JsonValue[] = [];
|
||||
if (this.#loaded === undefined)
|
||||
await this.#client.connect(this.transport, { timeout: 30_000 });
|
||||
const server = this.#client.getServerVersion();
|
||||
if (
|
||||
server?.name !== "jadx-headless-mcp" ||
|
||||
server.version !== JADX_RELEASE.version
|
||||
server?.name !== JADX_BRIDGE_IDENTITY.name ||
|
||||
server.version !== JADX_BRIDGE_IDENTITY.version
|
||||
)
|
||||
throw new AnalysisCapabilityUnavailableError(
|
||||
"jadx",
|
||||
request.operation,
|
||||
`Expected jadx-headless-mcp ${JADX_RELEASE.version}; server reported ${server?.name ?? "unknown"} ${server?.version ?? "unknown"}.`,
|
||||
`Expected ${JADX_BRIDGE_IDENTITY.name} ${JADX_BRIDGE_IDENTITY.version}; server reported ${server?.name ?? "unknown"} ${server?.version ?? "unknown"}.`,
|
||||
);
|
||||
const tools = this.#tools(request, target, signal);
|
||||
const loaded = jadxLoadSchema.parse(
|
||||
await tools.json("load_apk", {
|
||||
path: snapshot,
|
||||
threads: 1,
|
||||
resources: "full",
|
||||
}),
|
||||
const tools = this.#tools(request, target, signal, raw);
|
||||
const runtime = jadxRuntimeSchema.parse(
|
||||
await tools.json("rea_jvm_status", {}),
|
||||
);
|
||||
if (runtime.engine_reported_version !== JADX_RELEASE.version)
|
||||
throw new AnalysisCapabilityUnavailableError(
|
||||
"jadx",
|
||||
request.operation,
|
||||
`Expected engine ${JADX_RELEASE.version}; JAR reported ${runtime.engine_reported_version}.`,
|
||||
);
|
||||
const loaded =
|
||||
this.#loaded ??
|
||||
jadxLoadSchema.parse(
|
||||
await tools.json("load_apk", {
|
||||
path: snapshot,
|
||||
threads: 1,
|
||||
resources: "full",
|
||||
}),
|
||||
);
|
||||
if (loaded.apk_path !== snapshot)
|
||||
throw new AnalysisOutputError(
|
||||
request.operation,
|
||||
"JADX loaded a different APK path than the admitted snapshot",
|
||||
);
|
||||
this.#loaded = loaded;
|
||||
const engine = {
|
||||
name: "jadx-headless-mcp",
|
||||
version: JADX_RELEASE.version,
|
||||
version: runtime.engine_reported_version,
|
||||
artifact_sha256: jarHash,
|
||||
source_revision:
|
||||
jarHash === JADX_RELEASE.sha256 ? JADX_RELEASE.revision : null,
|
||||
worker_count: 1,
|
||||
heap_limit_mib: JADX_HEAP_LIMIT_MIB,
|
||||
heap_limit_mib:
|
||||
Math.floor(runtime.max_heap_bytes / (1024 * 1024)) || null,
|
||||
} as const;
|
||||
const result = await analyzeJadxRequest(tools, request, loaded, engine);
|
||||
return createAnalysisExecution(result, JADX_PROVIDER_IDENTITY, {
|
||||
rawResult: { server, jar_sha256: jarHash, calls: this.#raw },
|
||||
rawResult: {
|
||||
server,
|
||||
bridge_sha256: context.bridgeHash,
|
||||
jar_sha256: jarHash,
|
||||
loaded,
|
||||
calls: raw,
|
||||
},
|
||||
subject: target,
|
||||
limitations: JADX_LIMITATIONS,
|
||||
locations:
|
||||
@@ -115,6 +139,7 @@ export class JadxSession {
|
||||
request: AndroidRequest,
|
||||
target: BinaryTarget,
|
||||
signal: AbortSignal,
|
||||
raw: JsonValue[],
|
||||
): JadxToolPort {
|
||||
const call = async (
|
||||
name: string,
|
||||
@@ -126,7 +151,7 @@ export class JadxSession {
|
||||
{ timeout: 120_000, signal },
|
||||
);
|
||||
const envelope = parseJadxEnvelope(response, request.operation);
|
||||
this.#raw.push({
|
||||
raw.push({
|
||||
operation: name,
|
||||
input,
|
||||
response: jsonValueSchema.parse(response),
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { OfficialToolName } from "../contracts/officialToolContracts.js";
|
||||
import type { EnhancedToolName } from "../contracts/enhancedInputs.js";
|
||||
import type { NativeToolName } from "../contracts/nativeToolContracts.js";
|
||||
import type { ArtifactAnalysisOperation } from "../contracts/artifactToolContracts.js";
|
||||
import type { ManagedToolName } from "../contracts/managedToolContracts.js";
|
||||
import type { ManagedToolName } from "../contracts/managed/managedToolContracts.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user