Compare commits
177
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2f5fc6612 | ||
|
|
6847bc9473 | ||
|
|
9bab55337a | ||
|
|
c3bc38bf6c | ||
|
|
7a96391001 | ||
|
|
ab5a1c61b1 | ||
|
|
f04445c57c | ||
|
|
44b3627ab0 | ||
|
|
26881da76a | ||
|
|
d311940f24 | ||
|
|
cf061159ab | ||
|
|
5bcb6645b5 | ||
|
|
fd3282a87e | ||
|
|
a4e57366dd | ||
|
|
d565ecdc25 | ||
|
|
8189081511 | ||
|
|
7f1fa64019 | ||
|
|
c8aed4740d | ||
|
|
fa02df9ad3 | ||
|
|
ddea06360a | ||
|
|
94cc8475e0 | ||
|
|
1a24be574d | ||
|
|
217e8cf59f | ||
|
|
a55b5bb8ee | ||
|
|
ba35b5509d | ||
|
|
11d0ef2e09 | ||
|
|
805c338f36 | ||
|
|
a6a8f67649 | ||
|
|
7864d665e1 | ||
|
|
df6e476d67 | ||
|
|
ed3a7f070c | ||
|
|
132a3a46be | ||
|
|
e828acce0e | ||
|
|
53fa55937f | ||
|
|
e60aa11108 | ||
|
|
8b228ae701 | ||
|
|
03bc6d36db | ||
|
|
b6c21b1db5 | ||
|
|
433ebda8b4 | ||
|
|
2c1bf92010 | ||
|
|
7d4ed59816 | ||
|
|
7f1ae42f71 | ||
|
|
6ad62cec6a | ||
|
|
39db9fe424 | ||
|
|
c3d013b2fd | ||
|
|
a5d2986b1e | ||
|
|
6a37346acb | ||
|
|
f94fc7967b | ||
|
|
88a3c3f8a4 | ||
|
|
8dd1d84471 | ||
|
|
5802ef8746 | ||
|
|
3aaf7e2c54 | ||
|
|
fc80ea88df | ||
|
|
b89de8feab | ||
|
|
ae72abd019 | ||
|
|
ea0b733687 | ||
|
|
19f8cddfd6 | ||
|
|
9f42495d11 | ||
|
|
9b392bb2f8 | ||
|
|
5f27e02fae | ||
|
|
735c10059c | ||
|
|
77799e6c90 | ||
|
|
965d0b35f9 | ||
|
|
5295d56b10 | ||
|
|
dee8853fde | ||
|
|
494d4db6d9 | ||
|
|
e7f54b3310 | ||
|
|
3c84080aab | ||
|
|
55e5eb090b | ||
|
|
5e7f35c7c3 | ||
|
|
9c10a64f87 | ||
|
|
ba3869c597 | ||
|
|
38b293329c | ||
|
|
feeb19b8b3 | ||
|
|
2ef02ced16 | ||
|
|
1a936887b2 | ||
|
|
3fff74fb5b | ||
|
|
a574b744b8 | ||
|
|
bede3e3c60 | ||
|
|
de45f1dcb9 | ||
|
|
b3223e3f61 | ||
|
|
22acb0d5d7 | ||
|
|
1bd641d39f | ||
|
|
864205570a | ||
|
|
b81d18eeee | ||
|
|
c73742f300 | ||
|
|
adc07f5eaa | ||
|
|
0ce529af4e | ||
|
|
4a98c3ea03 | ||
|
|
12a2e3793a | ||
|
|
2b7bb3bb6a | ||
|
|
1b6f05470d | ||
|
|
5daaa453ff | ||
|
|
f557207887 | ||
|
|
5f3ba3e688 | ||
|
|
d1e5c0e1fd | ||
|
|
9997152c2d | ||
|
|
50f7e81432 | ||
|
|
a3bf4a0944 | ||
|
|
2919def828 | ||
|
|
d19686a2a5 | ||
|
|
bae87b4ded | ||
|
|
82895959d1 | ||
|
|
ffbde92f9a | ||
|
|
82957c98bf | ||
|
|
9c25dc4ed2 | ||
|
|
f3e6582e9e | ||
|
|
b37e47c9f7 | ||
|
|
6aba6ae231 | ||
|
|
ea9c3dab6b | ||
|
|
205a977777 | ||
|
|
6fa6f1f9b1 | ||
|
|
00fa97422f | ||
|
|
ebb4c45b68 | ||
|
|
28e0f4e9ec | ||
|
|
f73bb0fe11 | ||
|
|
348323dd23 | ||
|
|
8eac290e8d | ||
|
|
6256b68666 | ||
|
|
b225b0f64b | ||
|
|
bb91abd3df | ||
|
|
063cd25a93 | ||
|
|
52bf709bce | ||
|
|
3460eda9ca | ||
|
|
f5013c50ca | ||
|
|
eba709e5c0 | ||
|
|
e07b81649e | ||
|
|
c6d3bdf597 | ||
|
|
b5e98915c6 | ||
|
|
4f099a10f8 | ||
|
|
53fe7d6d3b | ||
|
|
778e464c20 | ||
|
|
ad86d39d2c | ||
|
|
e793e124a2 | ||
|
|
5b383b2ddd | ||
|
|
92617f9657 | ||
|
|
42e6a8a6f6 | ||
|
|
97822f233b | ||
|
|
09830dad05 | ||
|
|
5b59b87726 | ||
|
|
69a45e0bbc | ||
|
|
79bf26f33f | ||
|
|
ea58834940 | ||
|
|
828395d510 | ||
|
|
84e4361c09 | ||
|
|
05bb630a00 | ||
|
|
5b51fa01e2 | ||
|
|
333d118c0f | ||
|
|
4c8dc39f43 | ||
|
|
d40502aa3b | ||
|
|
4007b20fc2 | ||
|
|
3e4d4d93fd | ||
|
|
0f77ada55e | ||
|
|
9e30d93af3 | ||
|
|
c30878f616 | ||
|
|
6b7923cdd2 | ||
|
|
2df6b51ebb | ||
|
|
4200a425d5 | ||
|
|
9975bd23cd | ||
|
|
5b28a8b331 | ||
|
|
056c123759 | ||
|
|
4a5673e1dc | ||
|
|
385445804a | ||
|
|
bcfb4072f2 | ||
|
|
23c4eba5b3 | ||
|
|
80777d96da | ||
|
|
2fd11961cf | ||
|
|
55f0267c40 | ||
|
|
488fd5f60b | ||
|
|
d8c18f604f | ||
|
|
9c28f3c4c0 | ||
|
|
a8195ba2cc | ||
|
|
31ed005183 | ||
|
|
6a431d806a | ||
|
|
9d1c3631b8 | ||
|
|
405732a7f5 | ||
|
|
4fbc501121 |
+72
-20
@@ -105,7 +105,7 @@ jobs:
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: vitest-report-${{ strategy.job-index }}
|
||||
path: .vitest-reports/*
|
||||
path: .vitest/blob/*
|
||||
include-hidden-files: true
|
||||
if-no-files-found: error
|
||||
overwrite: true
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
- uses: actions/download-artifact@v4
|
||||
if: needs.changes.outputs.code == 'true' && success()
|
||||
with:
|
||||
path: .vitest-reports
|
||||
path: .vitest/blob
|
||||
pattern: vitest-report-*
|
||||
merge-multiple: true
|
||||
- name: Merge coverage, JUnit, and timing reports
|
||||
@@ -215,10 +215,31 @@ jobs:
|
||||
- name: Verify fixtures from a clean checkout
|
||||
run: npm run verify:fixtures
|
||||
|
||||
package-e2e:
|
||||
name: Package E2E (${{ matrix.os }})
|
||||
windows-native-build:
|
||||
name: Build bundled Windows x64 native controls
|
||||
needs: changes
|
||||
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Prepare cross compiler
|
||||
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64
|
||||
- run: npm run build:windows-native
|
||||
- run: npm run verify:windows-native:artifact
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-native-controls
|
||||
path: native/windows/build/
|
||||
if-no-files-found: error
|
||||
|
||||
package-e2e:
|
||||
name: Package/install integration (${{ matrix.os }})
|
||||
needs: [changes, windows-native-build]
|
||||
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -231,8 +252,31 @@ jobs:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: windows-native-controls
|
||||
path: native/windows/build/
|
||||
- run: npm run verify:windows-native:artifact
|
||||
- run: npm run verify:package
|
||||
|
||||
apple-artifacts:
|
||||
name: Real Apple artifact CLI/MCP E2E
|
||||
needs: changes
|
||||
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run verify:keyed-archive
|
||||
- run: npm run verify:asset-catalog
|
||||
- name: Real binary metadata and Interface Builder integration
|
||||
run: npm run verify:apple-dispatch && npm run verify:interface-builder
|
||||
|
||||
reconstruction-readiness:
|
||||
name: Reconstruction readiness
|
||||
needs: changes
|
||||
@@ -263,10 +307,14 @@ jobs:
|
||||
retention-days: 30
|
||||
|
||||
javascript-runtime-observation:
|
||||
name: JavaScript runtime observation
|
||||
name: JavaScript runtime observation (${{ matrix.os }})
|
||||
needs: changes
|
||||
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -280,7 +328,7 @@ jobs:
|
||||
|
||||
windows-curated:
|
||||
name: Windows capability and package lane
|
||||
needs: changes
|
||||
needs: [changes, windows-native-build]
|
||||
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 25
|
||||
@@ -293,6 +341,11 @@ jobs:
|
||||
- run: npm ci
|
||||
- run: npm run build:cached
|
||||
- run: npm run typecheck
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: windows-native-controls
|
||||
path: native/windows/build/
|
||||
- run: npm run verify:windows-native:artifact
|
||||
- name: Record named Windows host capabilities
|
||||
run: node scripts/windows-capabilities.mjs | Out-File -Encoding utf8NoBOM windows-capabilities.json
|
||||
- name: Validate Windows host capability report
|
||||
@@ -305,29 +358,33 @@ jobs:
|
||||
# ignores unmatched paths, so a stale entry would otherwise report green
|
||||
# while skipping coverage.
|
||||
#
|
||||
# Every path here must pass on windows-latest WITHOUT verified Windows
|
||||
# native authority. src/ghidra/GhidraProvider.test.ts,
|
||||
# src/ghidra/GhidraProvider.behavior.test.ts successors, and
|
||||
# tests/boundary/process/providerProcess.test.ts are deliberately absent:
|
||||
# they require Job Object process ownership and private-runtime DACL
|
||||
# enforcement, which this runner does not provide by design. Those claims
|
||||
# belong behind the same approval boundary as the real Ghidra Windows
|
||||
# workflow, not in the default lane.
|
||||
# The matching native artifact establishes real Job Object and private
|
||||
# runtime boundaries for these fixtures. Ghidra and Java are not needed;
|
||||
# real engine claims remain in the separate controlled-provider workflow.
|
||||
- name: Run curated Windows capability tests
|
||||
run: >-
|
||||
npx vitest run --coverage.enabled=false
|
||||
tests/boundary/filesystem/binaryTarget.test.ts
|
||||
tests/boundary/filesystem/setupUnavailableProvider.test.ts
|
||||
tests/boundary/filesystem/asarInventoryFreshness.test.ts
|
||||
tests/composition/javascript-applications/javascriptArtifactReconstruction.test.ts
|
||||
tests/conformance/providers/ghidra/windowsCapabilities.test.ts
|
||||
tests/conformance/providers/ghidra/ghidraWindowsFixture.test.ts
|
||||
src/ghidra/GhidraInstallation.test.ts
|
||||
tests/boundary/providers/ghidra/ghidraTransport.test.ts
|
||||
tests/boundary/providers/ghidra/ghidraTargetSnapshot.test.ts
|
||||
tests/boundary/providers/ghidra/ghidraLauncher.test.ts
|
||||
src/windows/WindowsNativeLoader.test.ts
|
||||
src/windows/WindowsOwnedProcess.test.ts
|
||||
src/ghidra/GhidraLauncherEnvironment.test.ts
|
||||
src/process/ProcessOwnership.test.ts
|
||||
src/process/ProcessOwnership.part2.test.ts
|
||||
src/process/ProcessOwnership.part3.test.ts
|
||||
src/process/ProcessOwnership.validation.test.ts
|
||||
src/application/Update.test.ts
|
||||
tests/process-global/updateMaintenance.test.ts
|
||||
tests/conformance/providers/ghidra/realGhidraWindowsWorkflow.test.ts
|
||||
- run: npm run verify:windows-native
|
||||
- run: npm run verify:package:windows
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
@@ -348,8 +405,3 @@ jobs:
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run docs:check
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: api-docs
|
||||
path: build/api-docs
|
||||
retention-days: 14
|
||||
|
||||
@@ -11,9 +11,29 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
native-artifact:
|
||||
name: Build matching Windows native artifact
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Prepare cross compiler
|
||||
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64
|
||||
- run: npm run build:windows-native
|
||||
- run: npm run verify:windows-native:artifact
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: real-ghidra-windows-native
|
||||
path: native/windows/build/
|
||||
if-no-files-found: error
|
||||
|
||||
verify:
|
||||
name: Real Ghidra 12.1.4 Windows x64 PE proof
|
||||
if: github.ref == 'refs/heads/main'
|
||||
needs: native-artifact
|
||||
environment: real-ghidra-windows
|
||||
runs-on: [self-hosted, Windows, x64, ghidra-12-1-2]
|
||||
timeout-minutes: 20
|
||||
@@ -23,13 +43,25 @@ jobs:
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- run: npm ci
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: real-ghidra-windows-native
|
||||
path: native/windows/build/
|
||||
- run: npm run verify:windows-native:artifact
|
||||
- run: npm run verify:windows-native
|
||||
- name: Verify all admitted Windows Ghidra operations
|
||||
env:
|
||||
REA_ANALYSIS_PROVIDER: ghidra
|
||||
run: npm run verify:ghidra:windows | Tee-Object -FilePath windows-ghidra-proof.log
|
||||
- name: Verify ordinary-user CLI and MCP operations
|
||||
env:
|
||||
REA_ANALYSIS_PROVIDER: ghidra
|
||||
run: npm run verify:ghidra:windows:package | Tee-Object -FilePath windows-ghidra-package-proof.log
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: windows-ghidra-proof
|
||||
path: windows-ghidra-proof.log
|
||||
path: |
|
||||
windows-ghidra-proof.log
|
||||
windows-ghidra-package-proof.log
|
||||
retention-days: 30
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
name: Real JavaScript replay verification
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/real-javascript-replay.yml"
|
||||
- "scripts/verify-real-javascript-replay.mjs"
|
||||
- "src/replay/**"
|
||||
- "src/application/JavaScriptReplay*"
|
||||
- "src/domain/javascriptReplay.ts"
|
||||
- "tests/fixtures/replay/**"
|
||||
- "package.json"
|
||||
- "package-lock.json"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: real-replay-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
HUSKY: "0"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
real-replay:
|
||||
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: sudo apt-get update && sudo apt-get install -y bubblewrap
|
||||
- run: npm run verify:replay
|
||||
env:
|
||||
REA_REPLAY_ALLOW_UNAVAILABLE: "true"
|
||||
@@ -108,6 +108,12 @@ jobs:
|
||||
|
||||
- run: npm ci
|
||||
|
||||
- name: Prepare Windows native build toolchain
|
||||
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64
|
||||
|
||||
- name: Build and verify bundled Windows native controls
|
||||
run: npm run build:windows-native && npm run verify:windows-native:artifact
|
||||
|
||||
- name: Verify packaged release before publishing
|
||||
run: npm run verify:package
|
||||
|
||||
@@ -129,23 +135,22 @@ jobs:
|
||||
verification_root="$(mktemp -d)"
|
||||
trap 'rm -rf -- "${verification_root}"' EXIT
|
||||
cd "${verification_root}"
|
||||
for attempt in $(seq 1 20); do
|
||||
published_version="$(npm view "rea-agents@${version}" version --json 2>/dev/null || true)"
|
||||
for attempt in $(seq 1 80); do
|
||||
published_version="$(npm view --prefer-online "rea-agents@${version}" version --json 2>/dev/null || true)"
|
||||
if [ "${published_version}" = "\"${version}\"" ]; then
|
||||
if npm exec --yes --package="rea-agents@${version}" -- rea --help \
|
||||
&& npm exec --yes --package="rea-agents@${version}" -- rea --llms \
|
||||
&& node "${GITHUB_WORKSPACE}/scripts/verify-published-package.mjs" "${version}"; then
|
||||
exit 0
|
||||
fi
|
||||
echo "Published rea-agents@${version} is visible, but CLI verification failed on attempt ${attempt}" >&2
|
||||
echo "rea-agents@${version} is visible on npm after attempt ${attempt}; verifying the published CLI."
|
||||
npm exec --yes --package="rea-agents@${version}" -- rea --help
|
||||
npm exec --yes --package="rea-agents@${version}" -- rea --llms
|
||||
node "${GITHUB_WORKSPACE}/scripts/verify-published-package.mjs" "${version}"
|
||||
exit 0
|
||||
else
|
||||
echo "rea-agents@${version} is not visible on npm yet (attempt ${attempt}/20)" >&2
|
||||
echo "rea-agents@${version} is not visible on npm yet (attempt ${attempt}/80; up to 20 minutes)" >&2
|
||||
fi
|
||||
if [ "${attempt}" -lt 20 ]; then
|
||||
if [ "${attempt}" -lt 80 ]; then
|
||||
sleep 15
|
||||
fi
|
||||
done
|
||||
echo "Published rea-agents@${version} failed registry and CLI verification" >&2
|
||||
echo "rea-agents@${version} did not become visible on npm within 20 minutes" >&2
|
||||
exit 1
|
||||
|
||||
publish-mcp:
|
||||
|
||||
+4
-2
@@ -6,8 +6,7 @@ __pycache__/
|
||||
# Distribution
|
||||
dist/
|
||||
build/
|
||||
docs/api/
|
||||
node_modules/
|
||||
node_modules
|
||||
*.egg-info/
|
||||
*.egg
|
||||
|
||||
@@ -42,3 +41,6 @@ test-results.xml
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Local reference sources and analysis artifacts
|
||||
/_reference/
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"_comment": "Test files are intentionally NOT excluded: the gate used to report zero clones because it skipped **/*.test.ts, hiding all test-tree duplication. Current actual is ~2.6% lines across src+ and tests/, down from 3.4% when tests were included. Lower this as duplication is removed.",
|
||||
"threshold": 2.7,
|
||||
"_comment": "Test files are intentionally NOT excluded: the gate used to report zero clones because it skipped **/*.test.ts, hiding all test-tree duplication. Current actual is ~2.4% lines across src+ and tests/, down from 3.4% when tests were included. Lower this as duplication is removed.",
|
||||
"threshold": 2.5,
|
||||
"reporters": ["consoleFull"],
|
||||
"ignore": [
|
||||
"node_modules/**",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||
"printWidth": 80,
|
||||
"sortImports": false,
|
||||
"sortPackageJson": false,
|
||||
"ignorePatterns": [
|
||||
"dist/**",
|
||||
"build/**",
|
||||
"docs/api/**",
|
||||
"node_modules/**",
|
||||
"CHANGELOG.md",
|
||||
"package-lock.json",
|
||||
"_reference/**"
|
||||
]
|
||||
}
|
||||
+21
-1
@@ -17,6 +17,15 @@
|
||||
"no-var-requires": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"scripts/electron-active-hook*.cjs",
|
||||
"tests/conformance/readiness/electron/*.js"
|
||||
],
|
||||
"rules": {
|
||||
"no-var-requires": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"src/{contracts,domain,server}/**/*.ts",
|
||||
@@ -120,5 +129,16 @@
|
||||
"node": true,
|
||||
"es2023": true
|
||||
},
|
||||
"ignorePatterns": ["dist", "node_modules", "docs/api", ".codex", ".devin"]
|
||||
"ignorePatterns": [
|
||||
"dist",
|
||||
"build",
|
||||
"node_modules",
|
||||
"docs/api",
|
||||
"coverage",
|
||||
".turbo",
|
||||
".cache",
|
||||
".codex",
|
||||
".devin",
|
||||
".factory"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
dist
|
||||
docs/api
|
||||
node_modules
|
||||
CHANGELOG.md
|
||||
package-lock.json
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "3.2.1"
|
||||
".": "4.0.1"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Product Direction
|
||||
|
||||
REA exposes reverse-engineering tools through a CLI and MCP server. Hopper and the bring-your-own Ghidra adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64 and has an experimental Windows x64 P0 boundary for approved native x86-64 PE applications; it supplies admitted read-only inventory and function-analysis operations but no GUI or mutation authority. Keep provider-specific code out of the domain and application layers.
|
||||
REA exposes reverse-engineering tools through a CLI and MCP server. Hopper and the bring-your-own Ghidra adapter are operation-capable deep binary-analysis providers. Ghidra is supported on Linux x64 and macOS x64/arm64 with matching native decompiler tools, and has an experimental Windows x64 P0 boundary for approved native x86-64 PE applications; on Linux and macOS it supplies inventory, function analysis, and atomic function annotation edits in an ephemeral database, without modifying executable bytes or controlling a GUI. Windows P0 has no mutation authority. Keep provider-specific code out of the domain and application layers.
|
||||
|
||||
Prioritize:
|
||||
|
||||
@@ -13,14 +13,14 @@ Prioritize:
|
||||
|
||||
Installers must not install or upgrade Homebrew, Node.js, npm, Java, Ghidra, or other unrelated software. Ghidra is bring-your-own. `rea setup` must print its planned changes and require approval before writing files or installing Hopper.
|
||||
|
||||
REA is a local-only tool; do not sanitize actionable local diagnostics such as artifact paths, digests, mismatch locations, or analysis metadata, while continuing to redact genuine secrets such as credentials and authorization headers.
|
||||
REA is a local-only tool. Preserve caller-selected inputs, captured output, URLs, paths, digests, mismatch locations, and analysis metadata. Do not guess that local evidence is secret from environment-variable names, argument names, or text patterns. Redact transport authentication credentials and values the caller explicitly marks sensitive; do not persist the entire ambient environment merely because a child inherits it.
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
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/browser/`, `src/replay/`, `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/browser/`, `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.
|
||||
@@ -30,9 +30,12 @@ REA is a layered ESM TypeScript application. Dependencies flow inward from pure
|
||||
|
||||
- `npm ci`: install the locked dependencies.
|
||||
- `npm run build:cached`: build the CLI and MCP server.
|
||||
- `npm run test:local`: run focused tests for changed code.
|
||||
- `npm run test:local`: run changed source tests without building; pass exact source test paths to run them regardless of Git status.
|
||||
- `npm run test:focused -- PATH...`: run exact test files; build first for boundary, acceptance, or process-global tests.
|
||||
- `npm run check:changed`: run cached static checks and source tests affected since the branch merge base (default `origin/main`).
|
||||
- `npm run check:fast`: run cached typecheck and lint checks.
|
||||
- `npm run check:pr`: run the deterministic PR gate and generated-document checks.
|
||||
- `npm run check:pr`: opt into the complete local deterministic gate and generated-document checks for broad changes; CI owns full coverage. Routine iterations need focused tests and relevant checks, not the whole gate each time.
|
||||
- `npm run docs:check`: check committed generated documents; `npm run docs:generate` regenerates them.
|
||||
- For provider-dependent changes, see [docs/testing.md](docs/testing.md) and run the matching real-provider verification.
|
||||
- Keep each verification lane's prerequisites limited to the claim it checks. Use host-native fixtures for host/provider acceptance; put optional cross-target formats and their external toolchains in a separate lane. Preflight required commands and report the missing dependency and lane clearly.
|
||||
|
||||
@@ -44,12 +47,28 @@ Configuration is parsed and validated by `src/config.ts` and `src/config/`. Keep
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
Use ESM TypeScript, two-space indentation, and Prettier defaults. Keep compiler strictness intact (`strict`, `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`, `verbatimModuleSyntax`). Use `camelCase` for values/functions, `PascalCase` for classes/types, and `UPPER_SNAKE_CASE` for constants. Parse unknown values at every MCP, environment, and subprocess boundary. Avoid `any`, unchecked casts, non-null assertions, import-time I/O, and floating promises. Exported APIs require concise JSDoc. Model expected failures with the tagged error algebra and `Result`, not broad exception wrappers.
|
||||
Use ESM TypeScript, two-space indentation, and the committed Oxfmt configuration. Keep compiler strictness intact (`strict`, `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`, `verbatimModuleSyntax`). Use `camelCase` for values/functions, `PascalCase` for classes/types, and `UPPER_SNAKE_CASE` for constants. Parse unknown values at every MCP, environment, and subprocess boundary. Avoid `any`, unchecked casts, non-null assertions, import-time I/O, and floating promises. Exported APIs require concise JSDoc. Model expected failures with the tagged error algebra and `Result`, not broad exception wrappers.
|
||||
|
||||
## Boundary Contracts
|
||||
|
||||
Treat a boundary as a contract between the producer's actual representation and the consumer's required meaning. When implementing or auditing a boundary, trace the value through parsing, normalization, authorization, serialization, and the CLI/MCP result. Establish affected callers from their code paths; similar tool names or workflows do not prove that they share a schema or failure mode.
|
||||
|
||||
Keep portable evidence and scenario validation distinct from host-native execution checks. Absolute filesystem paths, file URLs, and HTTP paths have different semantics; do not substitute one platform's syntax for the domain concept. Interpret provider metadata according to its documented or observed producer behavior. When a transformation loses information, preserve the reported value and an explicit unknown rather than guessing a canonical identity.
|
||||
|
||||
Preserve meaningful failure reasons through application and adapter layers. Malformed input is distinct from an unsupported target, unavailable provider, or host operating-system permission denial. Diagnostics should identify the failed constraint and the target or lifecycle request it applies to. Recovery advice must address that reason and point to an available workflow; generic catches must not erase actionable validation details.
|
||||
|
||||
Leave meaningful target, action, capture, and output choices to the agent. A selected operation already expresses intent; do not require approval booleans or repeated permission declarations. Trace each setting to its consumer: remove ignored options and single-value confirmations, derive built-in lifecycle behavior, and supply defaults for omitted optional metadata. Report actual effects and limitations where they help interpret results rather than asking callers to restate them.
|
||||
|
||||
Verify the representation consumed at the next boundary. Internal validator success does not establish JSON Schema validity or client compatibility: validate advertised input and output schemas against their declared dialect after SDK conversion, and keep generated contracts aligned. Use representative producer data and regressions that exercise the failed behavior. Platform and provider support claims require the corresponding real workflow; package startup, capability probes, and mock transport tests establish only their narrower claims. Report unverified coverage explicitly.
|
||||
|
||||
Bind tool handlers to named contracts, preserving their exact input and output types. Catalog ordering is presentation metadata; changing that order must not select a different schema or operation for an existing handler.
|
||||
|
||||
## Designing MCP Tools
|
||||
|
||||
Start from the analyst question and desired result, not a provider API. Before adding a tool, inspect the existing contract and its nearest alternative.
|
||||
|
||||
Let agents compose experiments with ordinary commands, scripts, and local fixture servers. REA tools should perform the requested inspection or capture and return evidence. A custom orchestration language, replay engine, or separate prepare/execute plan needs an observed requirement that those primitives cannot satisfy. A plan-only tool without an executor does not establish runtime behavior; expose the useful inspection directly.
|
||||
|
||||
- Prefer reusable, composable primitives: inspect one explicit object or
|
||||
relationship and return evidence that can support different analyst
|
||||
questions. Compose primitives into a workflow only for a recurring outcome
|
||||
@@ -57,7 +76,7 @@ Start from the analyst question and desired result, not a provider API. Before a
|
||||
business-domain interpretations out of general tool contracts.
|
||||
- Use **inspect/search** tools for facts about a target or candidate set; use **trace** tools for relationships; use **compare** tools for explicitly paired inputs.
|
||||
- Add a **workflow** when observed agent use shows a repeated sequence that REA can compose without losing analyst control. Return useful results inline so callers can choose their next action.
|
||||
- Use **observe/capture/replay** only when runtime activity is required, and declare authority and lifecycle effects in the contract.
|
||||
- Use **observe/capture** only when runtime activity is required, and declare authority and lifecycle effects in the contract.
|
||||
- Extend an existing tool when intent and result contract are unchanged. Add a tool for a distinct analyst outcome or materially different authority.
|
||||
- Keep caller-facing names and results provider-neutral. Put engine-specific behavior in provider adapters and report each provider's exact coverage.
|
||||
- Keep results complete by default. Add a limit only when it follows from a real format, protocol, authority, or resource-safety constraint; explain truncation and unsupported facets. Keep observed, derived, inferred, and unknown results distinct.
|
||||
@@ -68,10 +87,21 @@ See [docs/tool-design.md](docs/tool-design.md) for the design checklist. When us
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
Name tests `*.test.ts`. Use Vitest and production seams (`tests/fixtures/`) rather than module mocks. Domain tests assert pure behavior; adapter tests use fake launcher/socket seams; MCP tests connect with the client SDK version pinned in `package.json`. Preserve the canonical tool inventory defined by `TOOL_CONTRACTS` and verified through `CATALOG_IDENTITY` and generated product metadata. Cover malformed input, cancellation, lifecycle cleanup, and actual format, protocol, and permission boundaries. Do not add tests that merely freeze arbitrary caps or prescribed call sequences. Real Hopper, Ghidra, browser, JavaScript replay, managed conformance, and any real managed-tool claims cannot be replaced by mocks; use the corresponding `verify:*` command.
|
||||
Name tests `*.test.ts`. Use Vitest and production seams (`tests/fixtures/`) rather than module mocks. Domain tests assert pure behavior; adapter tests use fake launcher/socket seams; MCP tests connect with the client SDK version pinned in `package.json`. Preserve the canonical tool inventory defined by `TOOL_CONTRACTS` and verified through `CATALOG_IDENTITY` and generated product metadata. Cover malformed input, cancellation, lifecycle cleanup, and actual format, protocol, host-permission, and target-identity boundaries. Do not add tests that merely freeze arbitrary caps or prescribed call sequences. Real Hopper, Ghidra, browser, managed conformance, and any real managed-tool claims cannot be replaced by mocks; use the corresponding `verify:*` command.
|
||||
|
||||
Keep tool catalogs complete and self-describing; prefer capability- and session-scoped availability over schema truncation. Serialized bytes alone do not measure agent usability or model context cost.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
Use Conventional Commit subjects because Release Please derives versions and changelogs from them. Examples: `feat: add historical source import`, `fix(process): stop timers after exit`, and `docs: update architecture`. Use `!` or a `BREAKING CHANGE:` footer for breaking changes. Pull request titles must follow the same format because squash merges use the title as the release commit. Pull requests should describe contract or behavior changes, list verification commands, link issues, and include sanitized MCP examples when schemas change. State whether real Hopper/Ghidra verification was performed. Never commit binaries, Hopper or Ghidra project documents, credentials, `dist/`, `node_modules/`, or local planning artifacts (e.g. `.codex/`).
|
||||
|
||||
<!-- BEGIN:turborepo-agent-rules -->
|
||||
|
||||
# This is NOT the Turborepo you know
|
||||
|
||||
Turborepo configuration, task behavior, and CLI commands can vary between installed versions and may differ from your training data. Resolve the `turbo` package from this file's directory or relevant workspace; in monorepos, it may not be visible from the repository root. For example, run `node -p "require.resolve('turbo/package.json')"` from a workspace that depends on `turbo`.
|
||||
|
||||
Read `docs/README.md` inside that installed package first, then read the relevant pages from its `docs/` directory before changing Turborepo configuration or commands. Heed deprecation notices. These bundled docs match the installed package version and are available without network access.
|
||||
|
||||
This block is written and re-added by `turbo` before repository-scoped commands when an AI agent is detected. In the Turborepo source repository, its template is defined in `crates/turborepo-cli/src/cli/agent_guidance.rs`. Removing the managed block while updates are enabled means a later qualifying invocation will add it again. Set `"agentGuidance": false` in the root `turbo.json` or `turbo.jsonc` to opt out; this does not remove an existing block. Keep the block committed with your work to avoid an uncommitted change on the next agent invocation.
|
||||
<!-- END:turborepo-agent-rules -->
|
||||
|
||||
+134
@@ -1,5 +1,139 @@
|
||||
# Changelog
|
||||
|
||||
## [4.0.1](https://github.com/morluto/rea/compare/rea-agents-4.0.0...rea-agents-4.0.1) (2026-10-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **release:** allow npm propagation time ([2ef02ce](https://github.com/morluto/rea/commit/2ef02ced1603809f0fc6984612de31dce1801dd8))
|
||||
|
||||
## [4.0.0](https://github.com/morluto/rea/compare/rea-agents-3.2.1...rea-agents-4.0.0) (2026-10-05)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* Managed comparison metadata adds exact-signature matching and its count, and name_matching now reports exact-signature-fallback.
|
||||
* **setup:** unscoped setup refreshes only existing REA-owned registrations. Use --all-detected to configure every detected client. Setup JSON contains a compact doctor summary; use rea doctor for full diagnostics. Cancellation and successful dry runs now exit successfully.
|
||||
* Remove controlled replay, Node characterization, finite replay-machine, managed runtime planning, and redundant string-tracing tools. Retire process replay, shims, reactive scenarios, custom checkpoints, and active browser replay and origin-scope inputs.
|
||||
* **tools:** Removed permission configuration and policy commands, approval fields, filesystem scope inputs, and fixed confirmation options. Process capture inherits the host environment and accepts command names; callers must refresh tool schemas and migrate removed fields.
|
||||
|
||||
### Features
|
||||
|
||||
* **ghidra:** add DOS MZ analysis and complete function extents ([#556](https://github.com/morluto/rea/issues/556)) ([4f099a1](https://github.com/morluto/rea/commit/4f099a10f819125d52bd8baed9a9018496212eab))
|
||||
* **native:** add inspection primitives, dispatch traces and approved UI observation ([#500](https://github.com/morluto/rea/issues/500)) ([4fbc501](https://github.com/morluto/rea/commit/4fbc501121e96b33665a76cad2e5c3e8ac2e2c6d))
|
||||
* **setup:** expand agent integrations and verify updates ([#577](https://github.com/morluto/rea/issues/577)) ([6256b68](https://github.com/morluto/rea/commit/6256b6866616fa0c3137a77ebd31cf34155a642f))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **artifacts:** break directory child collation ties ([#587](https://github.com/morluto/rea/issues/587)) ([b37e47c](https://github.com/morluto/rea/commit/b37e47c9f729a66008b1dc8b0a1bb266e79c2776))
|
||||
* **artifacts:** exclude removed paths from unchanged counts ([#569](https://github.com/morluto/rea/issues/569)) ([52bf709](https://github.com/morluto/rea/commit/52bf709bcefb17ab07e1f9c47c5fadf5eca9bd4e))
|
||||
* **artifacts:** expand nested ASARs on Windows ([#582](https://github.com/morluto/rea/issues/582)) ([ebb4c45](https://github.com/morluto/rea/commit/ebb4c45b6892ab60f2c81e05fbb4cb528ffbffc4))
|
||||
* **artifacts:** interrupt ZIP extraction under backpressure ([#594](https://github.com/morluto/rea/issues/594)) ([50f7e81](https://github.com/morluto/rea/commit/50f7e81432001239ca8b5de3a9cfe8f5980d2711))
|
||||
* **artifacts:** preserve archive entry types and host paths ([#581](https://github.com/morluto/rea/issues/581)) ([28e0f4e](https://github.com/morluto/rea/commit/28e0f4e9ec09ae35b26920e4c6925f48770bf5ed))
|
||||
* **artifacts:** refresh cached ASAR headers for each inventory ([#574](https://github.com/morluto/rea/issues/574)) ([f5013c5](https://github.com/morluto/rea/commit/f5013c50ca81795f91fd945fae2fb76f9dbe0f97))
|
||||
* **artifacts:** resolve parents after archive enumeration ([#578](https://github.com/morluto/rea/issues/578)) ([8eac290](https://github.com/morluto/rea/commit/8eac290e8d716817526ab7f628e83c75fb33d06b))
|
||||
* **artifacts:** respect XML plist byte-order marks ([#514](https://github.com/morluto/rea/issues/514)) ([23c4eba](https://github.com/morluto/rea/commit/23c4eba5b38b8de4f9547d879f2c3e54f741b1e1))
|
||||
* **artifacts:** retain expanded ASAR container parents ([#583](https://github.com/morluto/rea/issues/583)) ([00fa974](https://github.com/morluto/rea/commit/00fa97422f585db878ffe5d1613c56608916f972))
|
||||
* **artifacts:** retain relative paths in directory identities ([#580](https://github.com/morluto/rea/issues/580)) ([348323d](https://github.com/morluto/rea/commit/348323dd23360bbac3841a9632ffcd08290566d2))
|
||||
* **browser:** capture popup network and report early event gaps ([#614](https://github.com/morluto/rea/issues/614)) ([f557207](https://github.com/morluto/rea/commit/f557207887ca9336f3d3c5fc02c76c354c1b2945))
|
||||
* **browser:** capture selected events in popup pages ([#600](https://github.com/morluto/rea/issues/600)) ([a3bf4a0](https://github.com/morluto/rea/commit/a3bf4a0944ce5a52e100470273b91fdd48ebce49))
|
||||
* **browser:** derive source-map imports from syntax nodes ([#588](https://github.com/morluto/rea/issues/588)) ([bae87b4](https://github.com/morluto/rea/commit/bae87b4ded343b34c4226b5e5d491145db7f85ff))
|
||||
* **browser:** distinguish duplicate screenshot frame events ([#612](https://github.com/morluto/rea/issues/612)) ([1bd641d](https://github.com/morluto/rea/commit/1bd641d39f58461566e4a3f07fa00f84f34e87ad))
|
||||
* **browser:** ignore duplicate frame navigations and read block-form source map directives ([#546](https://github.com/morluto/rea/issues/546)) ([828395d](https://github.com/morluto/rea/commit/828395d510bd4f29525e5a0807a550a2daad885c))
|
||||
* **browser:** keep subresource redirects out of page navigation scope ([#524](https://github.com/morluto/rea/issues/524)) ([9c28f3c](https://github.com/morluto/rea/commit/9c28f3c4c0840fd7424dc669db1fb9baa1dde5d7))
|
||||
* **browser:** preserve RGB PNG transparent samples ([#595](https://github.com/morluto/rea/issues/595)) ([9997152](https://github.com/morluto/rea/commit/9997152c2d3127162161a2b945f506f0e834be38))
|
||||
* **browser:** release cancelled CDP cleanup without waiting for responses ([#517](https://github.com/morluto/rea/issues/517)) ([488fd5f](https://github.com/morluto/rea/commit/488fd5f60be3b2900c27a915c73322b9cb5eabb5))
|
||||
* **browser:** release discarded source-map response bodies ([#611](https://github.com/morluto/rea/issues/611)) ([8642055](https://github.com/morluto/rea/commit/864205570a0a9452f96c11feacd00338d026f17b))
|
||||
* **browser:** resolve redirected source map paths from final URL ([#518](https://github.com/morluto/rea/issues/518)) ([056c123](https://github.com/morluto/rea/commit/056c1237591de3710cb96a51ddd71fa94317b906))
|
||||
* **cli:** fail commands returning projected analysis errors ([#506](https://github.com/morluto/rea/issues/506)) ([2fd1196](https://github.com/morluto/rea/commit/2fd11961cfba0c2d211a1aa643cbf05fa4e8c20d))
|
||||
* **cli:** reject invalid UTF-8 JSON file bytes ([#613](https://github.com/morluto/rea/issues/613)) ([22acb0d](https://github.com/morluto/rea/commit/22acb0d5d730379fbe49e9e491f446e11425190e))
|
||||
* **comparison:** account for unpaired records after explicit bundle pairing ([#603](https://github.com/morluto/rea/issues/603)) ([12a2e37](https://github.com/morluto/rea/commit/12a2e3793af63542448124311e17eacbd5235fc0))
|
||||
* **comparison:** match generated names without swallowing symbol prefixes ([#596](https://github.com/morluto/rea/issues/596)) ([5f3ba3e](https://github.com/morluto/rea/commit/5f3ba3e6886b6b3186711e025da14875dc3c9684))
|
||||
* **comparison:** normalize CFG identity by numeric addresses ([#593](https://github.com/morluto/rea/issues/593)) ([d1e5c0e](https://github.com/morluto/rea/commit/d1e5c0e1fd04d2d5a0d663cbe43787a010e3d783))
|
||||
* **comparison:** require decoded signatures for structural method matches ([#604](https://github.com/morluto/rea/issues/604)) ([4a98c3e](https://github.com/morluto/rea/commit/4a98c3ea035480561d138d79288974a860c2aa22))
|
||||
* **conformance:** preserve own JSON keys during comparison ([#573](https://github.com/morluto/rea/issues/573)) ([3460eda](https://github.com/morluto/rea/commit/3460eda9ca01ccf41e3b9895f3cbe975c0eaed51))
|
||||
* correct native inventories and bridge error classification ([#503](https://github.com/morluto/rea/issues/503)) ([405732a](https://github.com/morluto/rea/commit/405732a7f55e3033c29533b18f7d8313dbd28570))
|
||||
* **docs:** allow README installation wording to vary ([92617f9](https://github.com/morluto/rea/commit/92617f965720aaa74ec20ad67291adc5e83677a1))
|
||||
* **ghidra:** avoid nested quotes in Windows Java options ([a574b74](https://github.com/morluto/rea/commit/a574b744b8fe95e906524684f6a3b28cdc656499))
|
||||
* **ghidra:** recover evidenced switch cases and default targets ([#579](https://github.com/morluto/rea/issues/579)) ([b225b0f](https://github.com/morluto/rea/commit/b225b0f64bfa86586c17f193d30e278d8124b14e))
|
||||
* **inspector:** preserve lossy Node discovery locations ([#542](https://github.com/morluto/rea/issues/542)) ([84e4361](https://github.com/morluto/rea/commit/84e4361c0936a4bb30951699fb68970d4d7a2327)), closes [#530](https://github.com/morluto/rea/issues/530)
|
||||
* **javascript:** avoid invented computed destructuring properties ([#507](https://github.com/morluto/rea/issues/507)) ([80777d9](https://github.com/morluto/rea/commit/80777d96daf64d0266e5cf3f9aad9338b97be4ff))
|
||||
* **javascript:** bind named class expressions in their class scope ([#510](https://github.com/morluto/rea/issues/510)) ([55f0267](https://github.com/morluto/rea/commit/55f0267c409cbf096bcdbd07d7604c44bcde3fbe))
|
||||
* **javascript:** classify assignment targets and retain compound-assignment reads ([#537](https://github.com/morluto/rea/issues/537)) ([333d118](https://github.com/morluto/rea/commit/333d118c0fa65c8c418baaa337270c0ad25cd7cc))
|
||||
* **javascript:** correct lexical and dynamic scope analysis ([#538](https://github.com/morluto/rea/issues/538)) ([6fa6f1f](https://github.com/morluto/rea/commit/6fa6f1f9b17318220abb2683c327146785dcc5d3))
|
||||
* **javascript:** honor computed keys when naming callables, exports, requests and options ([#536](https://github.com/morluto/rea/issues/536)) ([4c8dc39](https://github.com/morluto/rea/commit/4c8dc39f439b5283b077c6aa47f262714d7c4bbd))
|
||||
* **javascript:** honor package exports condition order ([#591](https://github.com/morluto/rea/issues/591)) ([8289595](https://github.com/morluto/rea/commit/82895959d1494f16bc59909a2575facf3ab19d79))
|
||||
* **javascript:** isolate lexical loop bindings ([#511](https://github.com/morluto/rea/issues/511)) ([c30878f](https://github.com/morluto/rea/commit/c30878f616c4496c6cd1993d677aa019b2412b7e))
|
||||
* **javascript:** prefer package entrypoints over directory indexes ([#575](https://github.com/morluto/rea/issues/575)) ([bb91abd](https://github.com/morluto/rea/commit/bb91abd3df52802ec8f9d3d1fb215dce5eb6ea49))
|
||||
* **javascript:** preserve literal CommonJS path punctuation ([#576](https://github.com/morluto/rea/issues/576)) ([063cd25](https://github.com/morluto/rea/commit/063cd25a93ee9f16b45635fea196dde0d9309ab9))
|
||||
* **javascript:** preserve Node exports fallback stop reasons ([#606](https://github.com/morluto/rea/issues/606)) ([adc07f5](https://github.com/morluto/rea/commit/adc07f5eaab005f013c88617aa09b6f2b4af4111))
|
||||
* **javascript:** respect HTML base URL components ([#589](https://github.com/morluto/rea/issues/589)) ([ffbde92](https://github.com/morluto/rea/commit/ffbde92f9ab65a868b660abf17ce9d3c4577d125))
|
||||
* **javascript:** respect lexical shadowing of require ([#509](https://github.com/morluto/rea/issues/509)) ([6a431d8](https://github.com/morluto/rea/commit/6a431d806a1574a660b96ba49412b24df9e93b50))
|
||||
* **javascript:** retain property reads during updates ([#508](https://github.com/morluto/rea/issues/508)) ([6b7923c](https://github.com/morluto/rea/commit/6b7923cdd2a099d1481783f34970a3d1a71a8206))
|
||||
* **managed:** classify truncated signatures as malformed ([#598](https://github.com/morluto/rea/issues/598)) ([1b6f054](https://github.com/morluto/rea/commit/1b6f05470d6fb1903346ef7a25fb5f9d23079cb7))
|
||||
* **managed:** distinguish metadata handles from member accesses ([#607](https://github.com/morluto/rea/issues/607)) ([c73742f](https://github.com/morluto/rea/commit/c73742f3003ec6c1469f77ecc9ccb093df636b4a))
|
||||
* **managed:** recognize PE Thumb and ARMNT machines ([#605](https://github.com/morluto/rea/issues/605)) ([0ce529a](https://github.com/morluto/rea/commit/0ce529af4e0280fcbd4e647d0fcb0a9ca9f3496e))
|
||||
* **mcp:** emit valid schemas for empty arrays ([#535](https://github.com/morluto/rea/issues/535)) ([4007b20](https://github.com/morluto/rea/commit/4007b20fc277b4d20b52cae328dea58ae033be63)), closes [#528](https://github.com/morluto/rea/issues/528)
|
||||
* **mcp:** return complete inline Evidence records ([#585](https://github.com/morluto/rea/issues/585)) ([ea9c3da](https://github.com/morluto/rea/commit/ea9c3dab6b2ff4fa943c8ca4fec7ae8b304dbd93)), closes [#551](https://github.com/morluto/rea/issues/551)
|
||||
* **native:** capture Mach-O header metadata alongside load commands ([#525](https://github.com/morluto/rea/issues/525)) ([a8195ba](https://github.com/morluto/rea/commit/a8195ba2cc34a37f68d7bb8a8087d73ace40984b))
|
||||
* **native:** decode little-endian fat headers ([bede3e3](https://github.com/morluto/rea/commit/bede3e3c60e53d445209096b360a81eaa0c446cc)), closes [#592](https://github.com/morluto/rea/issues/592)
|
||||
* **native:** decode Mach protection bits in their proper positions ([#586](https://github.com/morluto/rea/issues/586)) ([6aba6ae](https://github.com/morluto/rea/commit/6aba6ae2316fe6299c855a3db52b221fff63c00c))
|
||||
* **native:** inspect Apple dispatch metadata in FAT64 binaries ([#590](https://github.com/morluto/rea/issues/590)) ([f3e6582](https://github.com/morluto/rea/commit/f3e6582e9ed327c5accb8f9068ea0eb9bac49b1f))
|
||||
* **native:** inspect the selected universal Mach-O architecture ([#601](https://github.com/morluto/rea/issues/601)) ([2b7bb3b](https://github.com/morluto/rea/commit/2b7bb3bb6a87704c18469768bc82d87e97b5c99c))
|
||||
* **native:** match compound otool keys exactly and badge unknown thread entrypoints ([#534](https://github.com/morluto/rea/issues/534)) ([d40502a](https://github.com/morluto/rea/commit/d40502aa3b9a8c87fde49e5887547066128f19ba))
|
||||
* **native:** normalize otool line endings and strip plist JSON BOM ([#533](https://github.com/morluto/rea/issues/533)) ([3e4d4d9](https://github.com/morluto/rea/commit/3e4d4d93fdc3eb5fdffda3a8dc8bec43201cdd98))
|
||||
* **native:** preserve text-valued load command metadata ([#520](https://github.com/morluto/rea/issues/520)) ([bcfb407](https://github.com/morluto/rea/commit/bcfb4072f2ff665e93477206e14ce4b0581f97b9))
|
||||
* **native:** reject signal-terminated command captures ([#608](https://github.com/morluto/rea/issues/608)) ([b81d18e](https://github.com/morluto/rea/commit/b81d18eeee9d93dafcb15e3d723de951509c2612))
|
||||
* **native:** reject trailing text in lipo integers and keep multi-word otool keys ([#531](https://github.com/morluto/rea/issues/531)) ([9e30d93](https://github.com/morluto/rea/commit/9e30d93af3d2c0cc1a7536a10e3141f9f2fb499c))
|
||||
* **native:** retain lazy dylibs, normalize version-min builds, limit thread entrypoints ([#532](https://github.com/morluto/rea/issues/532)) ([0f77ada](https://github.com/morluto/rea/commit/0f77ada55e6d1d01cb925993d871bf4a94f94bb2))
|
||||
* **native:** retain re-exported Mach-O library dependencies ([#516](https://github.com/morluto/rea/issues/516)) ([4a5673e](https://github.com/morluto/rea/commit/4a5673e1dc6f2213cf94121384cff405e390677a))
|
||||
* **package:** separate agent and Hopper setup support ([#584](https://github.com/morluto/rea/issues/584)) ([205a977](https://github.com/morluto/rea/commit/205a9777776476b4f361e38f25d1c75baaccd420))
|
||||
* **platform:** accept absolute paths for any host and stop blaming the ceiling for a missing grant ([#552](https://github.com/morluto/rea/issues/552)) ([e793e12](https://github.com/morluto/rea/commit/e793e124a27b8087057f58f39028144ea6f6e173))
|
||||
* **process:** align provider detach with configured platform ([3fff74f](https://github.com/morluto/rea/commit/3fff74fb5bb9725be2456e8c39bc0b98181d0687))
|
||||
* repair boundary contracts across CLI and MCP ([#597](https://github.com/morluto/rea/issues/597)) ([9c25dc4](https://github.com/morluto/rea/commit/9c25dc4ed2915a81fc83a1d4dc22e7ba814b8dff))
|
||||
* **replay:** keep __proto__ own properties and reject symbol-keyed results ([#540](https://github.com/morluto/rea/issues/540)) ([05bb630](https://github.com/morluto/rea/commit/05bb630a00aa521229cde53ded9a5345b8bd1a2b))
|
||||
* **replay:** link the complete ESM graph before evaluating ([#515](https://github.com/morluto/rea/issues/515)) ([4200a42](https://github.com/morluto/rea/commit/4200a425d50eab80847edf3baa9ea4c5244e8be8))
|
||||
* **replay:** preserve CommonJS object as ESM default export ([#519](https://github.com/morluto/rea/issues/519)) ([2df6b51](https://github.com/morluto/rea/commit/2df6b51ebb87e8d406d6201d8f3158aefefa7cc5))
|
||||
* **replay:** preserve Date call and explicit constructor semantics ([#512](https://github.com/morluto/rea/issues/512)) ([9975bd2](https://github.com/morluto/rea/commit/9975bd23cdba7e196ca97c3fcec41a6eee925e57))
|
||||
* **replay:** preserve sparse array positions in result projection ([#522](https://github.com/morluto/rea/issues/522)) ([9d1c363](https://github.com/morluto/rea/commit/9d1c3631b8deccb5fb91906130f38e3f24aa254e))
|
||||
* **replay:** share one seeded generator and present builtins with real identity ([#539](https://github.com/morluto/rea/issues/539)) ([5b51fa0](https://github.com/morluto/rea/commit/5b51fa01e2391cd4b6d5e9ba7b6aa7615de891b1))
|
||||
* **repo:** keep dependency links out of version control ([#617](https://github.com/morluto/rea/issues/617)) ([b3223e3](https://github.com/morluto/rea/commit/b3223e3f61a23b79152939ef755aca95e7640f09))
|
||||
* **skill:** remove obsolete grants and audit tool effects ([2919def](https://github.com/morluto/rea/commit/2919def828b3c8fc7947989f1de3f4f4ac4cdd36))
|
||||
* **test:** improve capture reliability and runner cleanup ([#609](https://github.com/morluto/rea/issues/609)) ([d19686a](https://github.com/morluto/rea/commit/d19686a2a5122c691f7885997906ecb31dd9827d))
|
||||
* **tools:** simplify direct invocation and harden boundary contracts ([#555](https://github.com/morluto/rea/issues/555)) ([b5e9891](https://github.com/morluto/rea/commit/b5e98915c60968213141f33839e0c5c4b34df4db))
|
||||
* **web:** retain uncertainty for incomplete capture inventories ([#513](https://github.com/morluto/rea/issues/513)) ([d8c18f6](https://github.com/morluto/rea/commit/d8c18f604f292c95b6b0b185d23517cb6881c5fb))
|
||||
* **win32:** probe .cmd/.bat runtime shims via cmd.exe to avoid spawn EINVAL ([#505](https://github.com/morluto/rea/issues/505)) ([31ed005](https://github.com/morluto/rea/commit/31ed005183c315f1bc72f6e10730a47be73d7f1c))
|
||||
* **workflows:** preserve punctuation in filesystem source paths ([#521](https://github.com/morluto/rea/issues/521)) ([5b28a8b](https://github.com/morluto/rea/commit/5b28a8b331259e026678e681210e9c3c4f48f2fa))
|
||||
* **workflows:** retain container presence in export shape comparisons ([#523](https://github.com/morluto/rea/issues/523)) ([3854458](https://github.com/morluto/rea/commit/385445804ab45605a47edcb996ebbc026e2f0a43))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **config:** take the environment as an input at every configuration read ([#554](https://github.com/morluto/rea/issues/554)) ([53fe7d6](https://github.com/morluto/rea/commit/53fe7d6d3b3630472124563ccafdad3f4964b4bc))
|
||||
* **domain:** consolidate duplicated semantic member and literal helpers ([#550](https://github.com/morluto/rea/issues/550)) ([09830da](https://github.com/morluto/rea/commit/09830dad053f2b1440e0b1cde63747606684be4b))
|
||||
* **domain:** own digest and prefixed identifier shapes in one module ([#547](https://github.com/morluto/rea/issues/547)) ([ea58834](https://github.com/morluto/rea/commit/ea5883494036ddb30d138fdb381d54b2d9808b49))
|
||||
* **host:** inject platform and environment instead of reading them ambiently ([#549](https://github.com/morluto/rea/issues/549)) ([5b59b87](https://github.com/morluto/rea/commit/5b59b87726b459cf9887dd4edc176981ef4e6afc))
|
||||
* simplify runtime tools and remove replay engines ([#572](https://github.com/morluto/rea/issues/572)) ([e07b816](https://github.com/morluto/rea/commit/e07b81649e96668e5ce166cfc6db3b59a7777697))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* add boundary contract guidance ([42e6a8a](https://github.com/morluto/rea/commit/42e6a8a6f6da4c3aebb10235a27d471dc2590954))
|
||||
* add Discord community links to README ([79bf26f](https://github.com/morluto/rea/commit/79bf26f33f980c20367986080a16d4a9f7952048))
|
||||
* align roadmap with direct tool use ([eba709e](https://github.com/morluto/rea/commit/eba709e5c096796b0afe3ce9203659df9eaf0a7d))
|
||||
* align translated READMEs and clarify support guides ([5b383b2](https://github.com/morluto/rea/commit/5b383b2ddd4195cb15e412b81853955b38419721))
|
||||
* clarify README wording and analysis tool support ([97822f2](https://github.com/morluto/rea/commit/97822f233be49a8b582b4c2aa982af3854ca0bb2))
|
||||
* move README image above Discord community ([ad86d39](https://github.com/morluto/rea/commit/ad86d39d2cb4f2d1ce0da08a17d5f7948c0797ec))
|
||||
* place community invitation below README navigation ([69a45e0](https://github.com/morluto/rea/commit/69a45e0bbc03d87624de866a821b976e4aaca069))
|
||||
* place Discord community below setup command ([778e464](https://github.com/morluto/rea/commit/778e464c206ba75a2f94edaad3c05e8bf63bed10))
|
||||
* place setup command above README image ([c6d3bdf](https://github.com/morluto/rea/commit/c6d3bdf597d9cd75a6f7396236e1a47f5ee4288d))
|
||||
|
||||
|
||||
### Tests
|
||||
|
||||
* **setup:** isolate client fixtures across host platforms ([f73bb0f](https://github.com/morluto/rea/commit/f73bb0fe116678543a73d13f58eaa3b48133afc5))
|
||||
|
||||
## [3.2.1](https://github.com/morluto/rea/compare/rea-agents-3.2.0...rea-agents-3.2.1) (2026-10-03)
|
||||
|
||||
|
||||
|
||||
+45
-29
@@ -10,7 +10,7 @@ REA development requires Node.js 24.18.x and npm 11.16.x. Real-Hopper verificati
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm test
|
||||
npm run check:fast
|
||||
```
|
||||
|
||||
`npm ci` installs the exact dependencies and prepares the Husky hooks without
|
||||
@@ -20,38 +20,46 @@ Turbo. Turbo caches deterministic builds and static checks across Git
|
||||
worktrees. After a package, lockfile, or managed-skill version change, run
|
||||
`npm run metadata:generate` before building.
|
||||
|
||||
Keep dependencies flowing inward through the existing domain, contracts, provider, application, server, and adapter layers. Parse unknown values at process and protocol boundaries, model expected failures with `Result`, and preserve the canonical tool inventory defined by `TOOL_CONTRACTS` unless a deliberate contract change updates every verifier, generated catalog artifact, and snapshot. Prefer capability- and session-scoped tool advertisement over schema truncation.
|
||||
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`.
|
||||
|
||||
Before submitting a pull request, run:
|
||||
## Development feedback and PR verification
|
||||
|
||||
For an ordinary edit, run the relevant regression and cached static checks:
|
||||
|
||||
```bash
|
||||
npm run check:pr
|
||||
npm run verify:package
|
||||
npm pack --dry-run
|
||||
npm run test:focused -- src/config.test.ts
|
||||
npm run check:fast
|
||||
```
|
||||
|
||||
`npm run check:fast` runs cached typecheck and lint for rapid local feedback.
|
||||
`npm run check` adds formatting, dead-code, and package-metadata freshness
|
||||
checks; use `npm run check:test` when the complete test suite is relevant.
|
||||
Pre-commit formats then lints staged source files, and pre-push runs
|
||||
`check:fast`. `check:pr` additionally renders API documentation and checks all
|
||||
committed generated metadata. Real
|
||||
provider and replay execution remains uncached, but their deterministic build
|
||||
prerequisite uses Turbo. CI uploads the rendered TypeDoc site as an `api-docs`
|
||||
artifact; the generated HTML is not committed.
|
||||
`test:local` selects dirty source tests without building; explicit source test
|
||||
paths run even on a clean tree. `check:changed` adds source tests affected since
|
||||
the branch merge base with `origin/main`. Use `npm run test:changed -- --base
|
||||
REVISION` to choose another base. Changed-test selection follows the import
|
||||
graph and is feedback, not complete correctness evidence. Inspect relevant
|
||||
boundary and provider behavior explicitly; see [docs/testing.md](docs/testing.md).
|
||||
|
||||
Local `npm test` runs every deterministic Vitest project without coverage,
|
||||
retries, or verbose output. Focused boundary, acceptance, changed-test, and
|
||||
watch commands are documented in [docs/testing.md](docs/testing.md), together
|
||||
with the path-based behavioral-depth rules. Changed-test selection follows the
|
||||
import graph and is feedback, not the release gate. CI splits the complete suite
|
||||
across four native Vitest shards, then merges coverage, JUnit, and JSON timing
|
||||
reports. Coverage thresholds remain in `vitest.config.ts`.
|
||||
Before handing off a PR, run the relevant tests, `npm run check`, and
|
||||
`npm run docs:check` when contracts or generated metadata change. Record which
|
||||
checks ran. CI owns complete deterministic tests and aggregate coverage. Use
|
||||
`npm run check:pr` for a deliberate full local gate on broad changes or when
|
||||
investigating CI failures; it is not required after each edit, rebase, or push.
|
||||
Packaging, setup, installation, or distribution changes also require
|
||||
`npm run verify:package` and `npm pack --dry-run`. Provider behavior changes
|
||||
require the matching real-provider `verify:*` lane.
|
||||
|
||||
CI installs dependencies once for all static checks, cancels superseded PR
|
||||
runs, and skips package, Windows, and full test lanes for documentation-only
|
||||
pull requests. TypeDoc renders only in pull-request CI; it does not run in local
|
||||
commit or pre-push hooks, or in post-merge `main` CI.
|
||||
`check:fast` runs cached typecheck and lint, reporting diagnostics on failure.
|
||||
`check` adds formatting, dead-code, and package-metadata freshness checks.
|
||||
Formatting uses Oxfmt and the committed `.oxfmtrc.json`; generated sources use
|
||||
the same configuration. Pre-commit formats and lints staged files; pre-push runs
|
||||
`check:fast`.
|
||||
`docs:check` checks committed generated metadata. `docs:generate` regenerates
|
||||
those files, and the docs CI lane checks them.
|
||||
Real-provider execution remains uncached; deterministic builds use Turbo.
|
||||
|
||||
Local `npm test` runs every deterministic Vitest project without coverage or
|
||||
retries. CI runs four coverage shards and merges JUnit and timing reports.
|
||||
CI cancels superseded PR runs and skips package, Windows, and full test lanes
|
||||
for documentation-only PRs. Coverage thresholds remain in `vitest.config.ts`.
|
||||
|
||||
Tests that need a temporary directory must use
|
||||
`createTestTempDirectory` from `tests/fixtures/temporaryDirectory.ts`. The
|
||||
@@ -92,13 +100,21 @@ Describe the behavior change and verification performed in the pull request. Nev
|
||||
|
||||
## Maintainer release checklist
|
||||
|
||||
Run the full checks, isolated package verifier, package dry run, and two-target real-Hopper verifier described above. Build a local tarball and exercise the executable through the package boundary:
|
||||
Run `npm run check:pr`, the isolated package verifier, package dry run, and two-target real-Hopper verifier described above. Build a local tarball and exercise the executable through the package boundary:
|
||||
|
||||
```bash
|
||||
npm pack
|
||||
npm exec --yes --package ./morluto-rea-0.1.0.tgz -- rea --help
|
||||
```
|
||||
|
||||
Use the exact filename printed by `npm pack` to run the packaged executable:
|
||||
|
||||
```bash
|
||||
npm exec --yes --package ./rea-agents-VERSION.tgz -- rea --help
|
||||
```
|
||||
|
||||
Replace `VERSION` with the packed version; do not use a tarball from an earlier
|
||||
build.
|
||||
|
||||
Publish the public package:
|
||||
|
||||
```bash
|
||||
@@ -110,6 +126,6 @@ After npm registry propagation, verify the published CLI and connect the client
|
||||
```bash
|
||||
npx -y rea-agents@latest --help
|
||||
npx -y rea-agents@latest doctor
|
||||
npx -y rea-agents@latest setup --yes
|
||||
npx -y rea-agents@latest setup --yes --all-detected
|
||||
npx -y rea-agents@latest mcp
|
||||
```
|
||||
|
||||
@@ -13,44 +13,54 @@
|
||||
[](#tool-catalog-for-investigation)
|
||||
[](https://nodejs.org/)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/82054/weekly?language=TypeScript" alt="morluto/rea | Trendshift" width="250" height="55" /></a>
|
||||
|
||||
[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)
|
||||
|
||||
<br />
|
||||
|
||||
<code>npm install --global rea-agents && rea setup</code>
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="REA launching its analysis bridge inside Hopper while inspecting a native binary" width="1200" />
|
||||
|
||||
<br />
|
||||
|
||||
<table aria-label="REA community">
|
||||
<tr>
|
||||
<td align="center" width="360">
|
||||
<a href="https://discord.gg/GkcryMnJDM">
|
||||
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
|
||||
<strong>Join the Reverse Engineering Community</strong>
|
||||
</a><br />
|
||||
<sub>Discord · Q&A · Show and Tell</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
See a feature in an app that you want in your own product? Give the app to your agent—even without its source code. With REA, the agent can investigate the feature, explain how it works, show its evidence, and build a version adapted to your stack and requirements.
|
||||
See a feature in an app that you want in your own product? Ask your agent to investigate it with REA. It can inspect the app without its source code, explain how the feature works, show the evidence, and build a version for your project.
|
||||
|
||||
REA gives agents one consistent way to investigate software. Today that includes deep native analysis and function dossiers through Hopper or bring-your-own Ghidra on Linux, plus an experimental Windows x64 Ghidra P0 for approved native PE applications; execution-free managed PE/CLI triage; reproducible Evidence records; controlled process capture; passive website, Electron page, and Node/Electron V8 Inspector observation; JavaScript/source-map reconstruction; and provider-neutral graphs for connecting application layers without confusing static inference with runtime observation. The longer-term toolkit extends the same agent workflow to APIs, protocols, mobile artifacts, firmware, richer runtime behavior, and differences between versions.
|
||||
REA connects your agent to tools for inspecting native binaries, JavaScript and Electron apps, .NET assemblies, and websites. You can also use the same tools from your terminal. Analysis runs locally, and results include the evidence and limitations behind each conclusion.
|
||||
|
||||
Reverse engineering normally makes the operator choose a tool, learn its API, move evidence between programs, and decide what to inspect next. REA gives that work to the agent through commands, skills, structured results, and repeatable investigation workflows.
|
||||
Setup configures your agent and connects it to Hopper or Ghidra. If you need an analysis tool, setup can install Hopper for you.
|
||||
|
||||
## Just ask your agent
|
||||
|
||||
Run setup once. Agent integration installs an aligned MCP registration and the
|
||||
bundled routing skill together:
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
Then ask:
|
||||
After [setup](#quick-start), restart your agent and ask:
|
||||
|
||||
```text
|
||||
Understand how search works in the Notes app, show me the evidence, and build a
|
||||
similar feature for my project.
|
||||
```
|
||||
|
||||
Notes is only an example. Name any app you want to understand, or ask the agent to start with an overview.
|
||||
Replace Notes with the app you want to understand, or ask for an overview first.
|
||||
|
||||
## The investigation model
|
||||
|
||||
@@ -79,119 +89,69 @@ REA shows how it reached its conclusions. It does not claim to recover original
|
||||
| ------------------------ | ----------------------------------------------------------------------------------------------------- |
|
||||
| **Built for agents** | Ask what an app does and let your agent inspect it instead of guessing. |
|
||||
| **CLI and MCP** | Run the same reverse-engineering capabilities from your terminal or agent. |
|
||||
| **Complexity handled** | REA installs and manages the reverse-engineering tools behind the scenes. |
|
||||
| **Guided setup** | Configure your agent, connect an existing analysis tool, or install Hopper with your approval. |
|
||||
| **From insight to code** | Understand a feature, then build your own version in the same coding session. |
|
||||
| **Local by design** | Analysis runs on your supported local host. REA does not upload the app to a hosted analysis service. |
|
||||
| **Keeps context** | Investigate several apps without starting over for every question. |
|
||||
|
||||
## Quick start
|
||||
|
||||
### Run setup — recommended
|
||||
### Run setup (recommended)
|
||||
|
||||
Set up REA with your agent:
|
||||
|
||||
```bash
|
||||
npx --yes rea-agents@latest setup
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
The npm package-runner prompt, when shown, approves downloading REA for this
|
||||
invocation; it does not approve any setup changes. The REA wizard separately
|
||||
shows its complete plan and asks before applying it. Setup does not update
|
||||
Homebrew, Node.js, or npm. The setup command opens with the work it
|
||||
enables: investigate local apps from an agent, recover evidence through a
|
||||
deep-analysis provider, and reuse REA's guided workflow. It summarizes the
|
||||
detected agents, then asks which capabilities to set up: agent integration
|
||||
(MCP plus the matching guided workflow) and—when needed—the Hopper provider.
|
||||
Nothing is preselected. Choosing agent integration opens a second empty
|
||||
checklist for the specific detected agents that should receive a registration.
|
||||
Choose which supported agents should use REA, then review the exact paths and changes before approving. Existing REA registrations are selected by default; newly detected agents are available to select, but detection alone does not select them. Setup adds MCP access and REA's guided workflow for selected agents. Hopper is a separate optional choice with its own consent. Setup can also record an existing Ghidra installation.
|
||||
|
||||
`@latest` makes the requested release explicit and asks npm for the release
|
||||
currently published under that tag. REA does not silently replace the package
|
||||
version npm selected. Intentional rollbacks therefore remain available through
|
||||
an exact package request.
|
||||
Setup shows its changes before applying them and backs up existing configuration. See [Installation and setup](docs/installation.md) for requirements and setup options.
|
||||
|
||||
REA keeps the journey inline so its history remains in the terminal. Selecting
|
||||
a capability does not select every detected target or authorize a change.
|
||||
Before anything changes, REA validates existing configuration, prints exact
|
||||
paths and external effects, and asks for final approval with **No** as the
|
||||
default. The screen keeps the available keys visible while you choose; Ctrl-C
|
||||
and declining leave the system unchanged.
|
||||
### With an agent (recommended)
|
||||
|
||||
REA detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, and Devin. It configures the first six when detected; Devin is reported but left unchanged because it has no documented local MCP configuration boundary. Registrations are additive, backup-first, and read back after writing. You can safely rerun setup.
|
||||
After setup, restart your agent and [describe the app or feature](#just-ask-your-agent) you want to understand. Hopper can run in demo mode; if it shows a first-run prompt, choose the demo or enter an existing license.
|
||||
|
||||
Use `rea setup --dry-run` to inspect the plan, repeat `--client` to select exact
|
||||
agents, and `--accessible` for sequential vertical prompts. Machine output
|
||||
remains available through `--json`; prompt UI and progress go to stderr.
|
||||
REA supports Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity, GitHub Copilot CLI, Command Code, and VS Code. Existing REA registrations are selected by default during setup; other detected agents remain unselected until chosen. Other agents can use the [manual MCP configuration](#manual-mcp-configuration).
|
||||
|
||||
After a successful setup, REA reports the capabilities now ready to use and a
|
||||
concrete next step, such as restarting a configured agent before asking it to
|
||||
investigate an application. It does not claim an integration or provider is
|
||||
ready unless setup and its final diagnostic check verified it.
|
||||
### From the terminal with npx
|
||||
|
||||
An optional curl wrapper installs the same CLI package and starts setup only when a terminal is available:
|
||||
After setup, run:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest doctor
|
||||
npx -y rea-agents@latest analyze /Applications/Notes.app
|
||||
```
|
||||
|
||||
### Install the rea command
|
||||
|
||||
Install the command-line interface:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
|
||||
```
|
||||
|
||||
Pass installer options after `bash -s --`, for example `--dry-run`, `--no-setup`, or `--version 1.0.0`. The curl wrapper never installs prerequisites or configures integrations itself. See [Installation and setup](docs/installation.md) for its exact mutation boundary.
|
||||
The installer adds `rea` to your system and starts setup when run in a terminal. It requires Node.js and npm to be installed already.
|
||||
|
||||
### With an agent — recommended
|
||||
|
||||
```bash
|
||||
npx --yes rea-agents@latest setup
|
||||
```
|
||||
|
||||
Choose Agent Integration in the reviewed setup plan. REA installs the pinned MCP
|
||||
registration and its matching routing skill as one transaction. After setup,
|
||||
restart the configured agent so it loads the aligned integration.
|
||||
|
||||
Review the setup plan, approve it if appropriate, then describe the app or feature you want to understand. Hopper can run in its free demo mode; if it shows a first-run prompt, choose the demo or enter an existing license.
|
||||
|
||||
### From Terminal — no installation
|
||||
|
||||
```bash
|
||||
npx --yes rea-agents@latest setup
|
||||
npx -y rea-agents@latest doctor
|
||||
npx -y rea-agents@latest analyze /Applications/Notes.app
|
||||
```
|
||||
|
||||
Review the setup plan before confirming it. Restart a configured agent so it loads REA.
|
||||
|
||||
### From Terminal — install the `rea` command
|
||||
Alternatively, install with npm, then run setup:
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea setup
|
||||
rea doctor
|
||||
rea analyze /Applications/Notes.app
|
||||
```
|
||||
|
||||
Update that global installation in place:
|
||||
|
||||
```bash
|
||||
rea upgrade
|
||||
```
|
||||
|
||||
REA checks npm for the latest release and verifies that the running package is
|
||||
the global installation it will replace. Source, local, and `npx` copies report
|
||||
the manual `npm install --global rea-agents@latest` command instead of updating
|
||||
an unrelated global package.
|
||||
|
||||
Choose either the no-install commands or the global installation. You do not need both.
|
||||
|
||||
`npm install rea-agents` without `--global` installs `rea` only into the
|
||||
current project's `node_modules/.bin`; it does not add `rea` to your shell
|
||||
`PATH`. Use the `npx` commands above for one-off runs or `--global` when you
|
||||
want a shell-visible `rea` command.
|
||||
Update either installation with `rea update`.
|
||||
|
||||
### Requirements
|
||||
|
||||
- macOS 12 or newer
|
||||
- Ubuntu 24.04+, Fedora 41+, or 64-bit Arch Linux
|
||||
- Windows x64 for the experimental, Ghidra-only native PE P0 boundary
|
||||
- Node.js 22.19+ or 24.11+ (including newer releases)
|
||||
- Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+
|
||||
- npm; REA does not require or install a particular npm version
|
||||
|
||||
Deep binary operations use [Hopper](https://www.hopperapp.com/), a separate desktop application with its own license, or a caller-selected Ghidra provider. Ghidra supplies read-only inventory, function metadata, decompilation, assembly, resolved calls, typed references, xrefs, CFG, and function dossiers; GUI state and mutations remain unavailable through that provider. Setup reuses an existing Hopper installation or an operator-supplied Ghidra installation. It never downloads Ghidra or installs Java. If neither provider is ready, interactive setup proposes Hopper; unattended Hopper installation requires `rea setup --yes --install-hopper`.
|
||||
Native binary analysis requires [Hopper](https://www.hopperapp.com/) or [Ghidra](#ghidra-analysis-provider). Hopper is separate software with its own license; its demo supports analysis with vendor-defined limits. REA can use Ghidra that you have already installed.
|
||||
|
||||
Windows Ghidra support is experimental for native x86-64 PE applications on local NTFS. The package bundles native Job Object, private-DACL, and path-admission controls. See [Windows Ghidra P0](docs/windows-ghidra-p0.md) for prerequisites and verified scope.
|
||||
|
||||
If something is not working, run:
|
||||
|
||||
@@ -199,13 +159,13 @@ If something is not working, run:
|
||||
npx -y rea-agents@latest doctor
|
||||
```
|
||||
|
||||
`rea doctor --json` is read-only and distinguishes unsupported hosts, missing dependencies, a missing local analysis engine, configuration drift, and healthy checks. Paid-license activation is optional: on Linux, REA runs the supported Hopper demo build on a private Xvfb display and selects Hopper's offered demo mode for each analysis session.
|
||||
`doctor` checks your host, dependencies, analysis tools, and agent configuration without changing them. Use `--json` for structured diagnostics.
|
||||
|
||||
### Linux installation and troubleshooting
|
||||
|
||||
On macOS, approved setup downloads Hopper's official DMG, verifies it, and installs the app into `~/Applications` without Homebrew or administrator privileges. Hopper may show its demo or license prompt when first opened; no manual drag-and-drop is required.
|
||||
On macOS, setup can install Hopper in `~/Applications` after approval. It verifies the official download and does not need Homebrew or administrator privileges.
|
||||
|
||||
On Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux, approved setup downloads the pinned official Hopper 6.4.2 package, restricts downloads to Hopper's public origin, verifies the published size and checksum, and invokes `apt-get`, `dnf`, or `pacman` to install Hopper and the Xvfb, Python, X11, and XTEST packages used by demo sessions. When REA is not already running as root, `pkexec` presents the system authorization prompt. REA never invokes `sudo`. Demo sessions run on an isolated 1280×1024 Xvfb display. REA verifies the exact supported Hopper binary, its owned process ancestry, the expected dialog geometry, and bridge state before selecting `Try the Demo`; any mismatch fails closed.
|
||||
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:
|
||||
|
||||
@@ -220,13 +180,15 @@ If doctor reports a missing analysis engine even though the file exists, inspect
|
||||
ldd /opt/hopper/bin/Hopper | grep 'not found'
|
||||
```
|
||||
|
||||
Install the missing distribution packages and rerun `rea setup`. Linux demo automation requires `Xvfb`, Python 3, `libX11.so.6`, and `libXtst.so.6`; approved setup installs those direct runtime dependencies and does not interact with the user's desktop display. Hopper's free demo supports analysis with vendor-defined limits, and a paid license is optional. The curl installer places the `rea` command in `~/.local/bin` on Linux; add that directory to future shell `PATH` values if it is not already present.
|
||||
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.
|
||||
|
||||
### Ghidra read-only analysis provider
|
||||
### Ghidra analysis provider
|
||||
|
||||
The Ghidra adapter supports the exact official Ghidra 12.1.4 release with a 64-bit full JDK 21 on Linux x64. macOS is not an admitted Ghidra host. The adapter also provides an experimental Windows x64 P0 limited to approved native x86-64 PE applications. Download and extract those projects yourself, then configure absolute paths:
|
||||
Already use Ghidra? REA can connect it to your agent on Linux x64 or macOS x64/arm64. It requires **Ghidra 12.1.4** and a **64-bit JDK 21**. On macOS, your Ghidra installation must also include the native decompiler for your architecture.
|
||||
|
||||
Set the installation paths, then run setup:
|
||||
|
||||
```bash
|
||||
export GHIDRA_INSTALL_DIR=/absolute/path/to/ghidra_12.1.4_PUBLIC
|
||||
@@ -236,21 +198,15 @@ rea setup
|
||||
rea providers --json
|
||||
```
|
||||
|
||||
Doctor distinguishes missing configuration, a bad installation root, the wrong Ghidra or Java version, a JRE without `javac`, a missing `support/analyzeHeadless`, and an unsupported platform or architecture. Approved setup only copies the verified non-secret paths into detected MCP registrations; it does not modify the Ghidra installation or install/download Ghidra or Java.
|
||||
Setup checks the installations and saves their paths in your selected agents' configuration after approval. Ghidra and Java must already be installed; REA does not download or change them.
|
||||
|
||||
On Windows, set the same variables in PowerShell and run `rea doctor --json`; automated `rea setup` and Hopper installation remain unavailable. The P0 target boundary rejects DLLs, managed PE files, non-x86-64 images, mutable/hostile inputs, and non-PE formats. See the [Windows Ghidra P0 operations guide](docs/windows-ghidra-p0.md) for registration, exact limitations, CI evidence, and acceptance gates.
|
||||
The adapter exposes inventory, function, memory and load-image inspection, plus atomic function annotation edits on Linux and macOS. `annotate_native_function` edits names and entry comments in the session database and returns a refreshed function dossier; executable bytes stay unchanged. Ghidra does not provide GUI controls through REA.
|
||||
|
||||
REA loads its packaged Java `HeadlessScript` with `-scriptPath`, copies and digest-verifies the target in an ephemeral runtime, enables `-readOnly` and `-deleteProject`, and authenticates every request. Auto-analysis completes before operations are served; startup has a deadline, while tool requests run until a result, caller cancellation, or provider shutdown. Linux uses a mode-0600 Unix socket. Windows P0 uses token-authenticated IPv4 loopback and a token-free endpoint record because Node path-based IPC does not connect to Java AF_UNIX sockets on Windows. The bridge verifies Ghidra's imported-byte SHA-256 before serving any operation.
|
||||
REA analyzes a temporary copy of the target and removes the temporary project when the session closes. Results identify what Ghidra observed and what it could not resolve. Decompilation produces pseudocode rather than the original source.
|
||||
|
||||
The Ghidra adapter declares 19 direct and enhanced operations. Its ten inventory operations are `list_documents`, `list_procedures`, `list_strings`, `list_names`, `list_segments`, `address_name`, `procedure_address`, `resolve_containing_procedure`, `search_procedures`, and `search_strings`. It also admits `procedure_info`, `procedure_pseudo_code`, `procedure_assembly`, `read_function_instructions`, `procedure_callers`, `procedure_callees`, `procedure_references`, `xrefs`, and `analyze_function`. `read_function_instructions` is the offset-paginated fast path for raw instruction windows: it does not invoke the decompiler or whole-program name/string inventories, and is also exposed as `rea instructions`. These capabilities enable the shared Swift/Objective-C inventory workflows, `binary_overview`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, `trace_feature`, and complete function dossiers. Default-space addresses are lowercase `0x` hexadecimal. Other spaces, including `EXTERNAL`, use `<percent-encoded-space>:0x<hex>`. Symbol results identify primary, dynamic, external, type, and source facts; procedures distinguish external functions and thunks; strings identify charset, missing-terminator state, byte length, and value truncation; memory-block ends are exclusive and permissions come directly from Ghidra.
|
||||
Ghidra also imports DOS MZ executables with an explicit 16-bit x86 real-mode profile. Function results include complete observed body ranges, distinguishing owned bytes from the enclosing span. See the [DOS analysis guide](docs/ghidra-dos.md) for addresses, packing, and verification boundaries.
|
||||
|
||||
The bridge serves operations only after auto-analysis completes. Each Program owns one persistent `DecompInterface`, and a serial FIFO keeps Ghidra API access on the owning program's thread; it has no fixed queue length or per-operation deadline. Reference results preserve Ghidra's call/jump/data/read/write/indirect/computed/external facts, while unresolved targetless flows remain explicitly unknown. Synthetic entry-point references without actionable memory sources are omitted. Pseudocode and assembly are provider-specific observations, not original source or Hopper-equivalent text. Caller cancellation and provider shutdown remain available; provider results are returned without a fixed response-size ceiling.
|
||||
|
||||
`npm run verify:ghidra` builds debug and stripped fixtures for the supported Linux x64 ELF host. Against real Ghidra 12.1.4 it validates every admitted operation, direct and indirect calls, imports/exports/thunks, typed references, strings/xrefs, multi-block CFG, cancellation, deadlines, concurrency, malformed inputs, and complete process/project cleanup. It needs a host C compiler and the Ghidra/JDK prerequisites above.
|
||||
|
||||
`npm run verify:ghidra:cross-format` adds AArch64 ELF, x86-64 PE, and x86-64 Mach-O fixtures. This separate lane requires `clang`, LLD, and `lld-link`; use `REA_CLANG`, `REA_LLD`, or `REA_LLD_LINK` to select alternate command paths. It preflights the required toolchain before compiling fixtures.
|
||||
|
||||
`npm run verify:ghidra:windows` uses a deterministic source-owned native x86-64 PE application and requires all 19 operations, target/snapshot/import digest linkage, authenticated loopback transport, and cleanup on a controlled Windows x64 Ghidra 12.1.4 runner. This proof does not establish Job Object ownership, private DACLs, or reparse-point-safe authority.
|
||||
Windows Ghidra P0 uses bundled native controls for its read-only native x86-64 PE boundary on local NTFS; see the [Windows Ghidra P0 guide](docs/windows-ghidra-p0.md). See [Ghidra installation](docs/installation.md#ghidra), [provider evaluation](docs/provider-evaluation.md), and [testing](docs/testing.md) for configuration details, coverage, and real-provider verification.
|
||||
|
||||
To remove only REA-owned MCP registrations and the managed skill:
|
||||
|
||||
@@ -263,15 +219,15 @@ Uninstall preserves Hopper, Node.js, Evidence files, captures, unrelated skills,
|
||||
|
||||
### CLI or agent?
|
||||
|
||||
| If you want to… | Use |
|
||||
| ---------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| Ask an agent to investigate an app and build a feature | Install the skill, then talk to your agent |
|
||||
| Inspect or decompile one part of an app from the Terminal | `rea analyze` or `rea decompile` |
|
||||
| Validate, canonicalize, or compare Evidence bundles | `rea evidence-import`, `rea evidence-export`, or `rea compare` |
|
||||
| Map a local JavaScript/Electron application without executing it | `rea analyze PATH` or `rea analyze-javascript-application` |
|
||||
| Reuse immutable analysis results without relaunching a provider | Pass `--snapshot /approved/path/analysis.json` to a deep-analysis command |
|
||||
| Import source as historical reference | `rea import-reference-source` |
|
||||
| Capture or compare controlled process behavior | `rea capture-process` or `rea compare-process-captures` |
|
||||
| If you want to… | Use |
|
||||
| ---------------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| Ask an agent to investigate an app and build a feature | Run setup, restart your agent, then describe the task |
|
||||
| Inspect or decompile one part of an app from the Terminal | `rea analyze` or `rea decompile` |
|
||||
| Validate, canonicalize, or compare Evidence bundles | `rea evidence-import`, `rea evidence-export`, or `rea compare` |
|
||||
| Map a local JavaScript/Electron application without executing it | `rea analyze PATH` or `rea analyze-javascript-application` |
|
||||
| Reuse immutable analysis results without relaunching a provider | Pass `--snapshot /path/to/analysis.json` to a deep-analysis command |
|
||||
| Import source as historical reference | `rea import-reference-source` |
|
||||
| Capture or compare controlled process behavior | `rea capture-process` or `rea compare-process-captures` |
|
||||
|
||||
```bash
|
||||
rea evidence-import /absolute/path/to/evidence/bundle.json
|
||||
@@ -279,8 +235,7 @@ rea evidence-export /absolute/path/to/evidence/bundle.json /absolute/path/to/evi
|
||||
rea compare /absolute/path/to/evidence/left.json /absolute/path/to/evidence/right.json
|
||||
```
|
||||
|
||||
JavaScript application analysis reads the selected directory or ASAR directly;
|
||||
the command uses the supplied path directly:
|
||||
Analyze a JavaScript application directory or ASAR without executing it:
|
||||
|
||||
```bash
|
||||
rea analyze /absolute/path/to/releases/app.asar --json
|
||||
@@ -292,25 +247,15 @@ static JavaScript application provider when neither `--provider` nor
|
||||
`--snapshot` is supplied. Both routes return the analysis and its Evidence
|
||||
context inline.
|
||||
|
||||
Historical source import takes the directory directly and never treats source
|
||||
as current behavioral authority:
|
||||
Import an older source tree as a reference. REA keeps it separate from observations of the current app:
|
||||
|
||||
```bash
|
||||
rea import-reference-source /absolute/path/to/source
|
||||
```
|
||||
|
||||
Imports read the path supplied to the command and validate every Evidence ID and manifest. Exports never replace an existing file unless `--overwrite` is explicit.
|
||||
Imports read the path supplied to the command. File names do not cause automatic omissions; files are represented by hashes and metadata. To exclude selected paths, set `REA_REFERENCE_SECRET_PATTERNS_JSON` to a JSON string array of ignore patterns. Exports never replace an existing file unless `--overwrite` is explicit.
|
||||
|
||||
Provider-neutral analysis snapshots persist successful, immutable REA calls and
|
||||
their Evidence records. They are exact caches rather than Hopper databases:
|
||||
REA reuses a v2 entry only when the binary digest, kind, format, architecture,
|
||||
operation parameters, concrete provider build, and canonical analysis-profile
|
||||
digest match. Hopper loader defaults and configured overrides are normalized by
|
||||
the Hopper adapter and committed to that profile, so overrides occupy a distinct
|
||||
safe cache partition instead of disabling snapshots. Cursor-dependent and
|
||||
mutating calls are never cached. Snapshot files can contain proprietary analysis
|
||||
results and local paths, so REA keeps them local and writes them with owner-only
|
||||
permissions. The caller supplies the snapshot path directly:
|
||||
Use a snapshot to save successful analysis results and reuse them on later runs. REA reuses a result only when the target bytes, operation, parameters, analysis tool, and settings match. It does not cache changes or cursor-dependent calls. Snapshot files stay local and use owner-only permissions.
|
||||
|
||||
```bash
|
||||
rea analyze /absolute/path/to/app --snapshot /absolute/path/to/analysis/app.json
|
||||
@@ -318,10 +263,10 @@ rea analyze /absolute/path/to/app --snapshot /absolute/path/to/analysis/app.json
|
||||
rea analyze /absolute/path/to/app --snapshot /absolute/path/to/analysis/app.json
|
||||
```
|
||||
|
||||
Exact CLI evidence replays happen before any provider process starts. In MCP sessions,
|
||||
Exact CLI cached-evidence reads happen before any provider process starts. In MCP sessions,
|
||||
pass `snapshot_path` to `open_binary` to import a snapshot atomically while
|
||||
opening its matching target; MCP providers may still start before a cached call
|
||||
is replayed. Pass `snapshot_path` and, when required, `overwrite: true` to
|
||||
result is returned. Pass `snapshot_path` and, when required, `overwrite: true` to
|
||||
`close_binary` to save atomically before Hopper resources are released. If the
|
||||
save fails, REA deliberately leaves the session open.
|
||||
|
||||
@@ -343,7 +288,7 @@ REA gives the agent a clear path from that request to working code:
|
||||
| 5 | Decompiles the relevant routines | `procedure_pseudo_code`, `procedure_assembly`, `batch_decompile` |
|
||||
| 6 | Builds the feature in your project | code adapted to your stack, product, and requirements |
|
||||
|
||||
REA handles the app analysis in steps 1–5. The agent performs step 6 with its normal file-editing and test tools, using what it learned about the app.
|
||||
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.
|
||||
|
||||
## What agents can do
|
||||
|
||||
@@ -356,155 +301,119 @@ REA handles the app analysis in steps 1–5. The agent performs step 6 with its
|
||||
- Analyze Swift and Objective-C metadata without manually untangling every mangled symbol.
|
||||
- Leave names, comments, and bookmarks in Hopper so human and agent analysis reinforce each other.
|
||||
|
||||
See [native investigation](docs/native-investigation.md) for keyed archives, instruction/call/type primitives, typed dispatch metadata, value traces and native desktop observation.
|
||||
|
||||
## Tool catalog for investigation
|
||||
|
||||
| Tool family | Count | Examples |
|
||||
| ------------------------- | ----: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Native inspection | 36 | procedures, pseudocode, assembly, strings, names, segments, callers, callees, xrefs, annotations, bounded byte reads, file-offset translation |
|
||||
| Investigation workflows | 14 | `binary_overview`, `analyze_function`, `inspect_native_api`, `inspect_native_dispatch_metadata`, `batch_decompile`, `trace_feature`, `trace_native_investigation`, exact string-to-code lookup, bounded call paths, call graphs, Swift and Objective-C discovery |
|
||||
| Native macOS utilities | 5 | Mach-O metadata, code signatures, plists, architectures, Swift demangling; Hopper-free and provenance-bearing |
|
||||
| Artifact graph | 3 | complete inline inspection of directories and supported packages, compiled Interface Builder UI graph decoding, plus explicitly selected extraction into an absent owned tree |
|
||||
| Managed PE/CLI | 8 | PE/CLI identity, metadata members, CIL hashes, P/Invoke/native-boundary declarations and verification, application-graph projection, decompiler reconstruction import, token remapping, runtime-correlation plans, and version comparison |
|
||||
| Browser observation | 9 | exact-origin passive CDP capture, bundle and source-map analysis, WebMCP discovery, session timelines, capture diff, visual evidence, and bounded Playwright scenarios |
|
||||
| Electron analysis | 5 | passive root-confined observation, static application mapping, evidence-backed static/runtime reconciliation, and provider-owned click/wait scenarios |
|
||||
| JavaScript runtime | 2 | approved attach-only Node/Electron Inspector target discovery plus bounded script and execution-context observation without evaluation or instrumentation |
|
||||
| Application workflows | 10 | complete cross-layer traces, unique-only version matching, historical-source to bundle mapping, static export return-shape comparison, approved Linux-isolated extracted-module replay, managed-runtime characterization, reconstruction coverage closure, and deterministic obligation ledgers |
|
||||
| Workspace and observation | 22 | target lifecycle, inline Evidence bundle retrieval, aggregate navigation/address context, direct finite replay-machine evaluation, process/artifact/function comparison, evidence-linked residual-unknown lifecycle |
|
||||
| Tool family | Count | Examples |
|
||||
| ------------------------- | ----: | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Native inspection | 41 | functions, pseudocode, assembly, strings, symbols, calls, references, annotations, byte reads, and file offsets |
|
||||
| Investigation workflows | 14 | app overviews, function dossiers, native APIs and dispatch, batch decompilation, feature traces, call paths, call graphs, Swift and Objective-C discovery |
|
||||
| Native macOS utilities | 7 | Mach-O metadata, code signatures, plists, architectures, and Swift demangling without launching Hopper |
|
||||
| Artifact graph | 5 | directory and package inventories, compiled Interface Builder files, Apple asset catalogs, and extraction |
|
||||
| Managed PE/CLI | 7 | .NET identity, metadata, CIL instructions, native dependencies, reconstruction imports, and build comparisons |
|
||||
| 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 | 7 | 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 implements controlled behavioral capture.
|
||||
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.
|
||||
|
||||
## Current status
|
||||
|
||||
REA is already useful for native application, browser, and Electron investigation on supported macOS and Linux hosts, plus the bounded Windows Ghidra P0 described above:
|
||||
REA supports native application, JavaScript, Electron, .NET, and browser investigation on macOS and Linux. Individual tools have platform and runtime prerequisites; use `rea capabilities` to check what is available on your host.
|
||||
|
||||
- Open Mach-O, ELF, PE, `.app`, ZIP, APK, IPA, ASAR, plist, JavaScript, source-map, and generic analysis-database targets; Hopper remains the only adapter that accepts legacy `.hop` databases.
|
||||
- Discover deep-analysis candidates without starting them, choose deterministically, and retain one immutable provider/profile binding until an explicit switch or close; provider failures never trigger transparent fallback.
|
||||
- Attach to a user-owned Chrome-family browser over a configured loopback CDP endpoint; capture exact-origin web structure, safe metadata, approved value-free payload shapes, bundle/source-map evidence, WebMCP declarations, user-action timelines, capture diffs, and explicitly approved screenshots without navigation or JavaScript evaluation.
|
||||
- Inspect Electron `file://` renderer pages through a separate canonical-root permission boundary without invoking Electron APIs; script contents remain separately approved and byte bounded.
|
||||
- Attach to one exact approved Node or Electron V8 Inspector target and retain bounded `scriptParsed` plus execution-context lifecycle metadata without evaluation, breakpoints, resume, source reads, or instrumentation. require/import edges, EventEmitter activity, Electron IPC, PID identity, and role identity remain unknown. See [passive Node and Electron runtime observation](docs/javascript-runtime-observation.md).
|
||||
- Validate and canonically serialize a provider-neutral [JavaScript Application Graph](docs/javascript-application-graph.md) spanning packages, ASAR entries, Electron roles, JavaScript/source-map entities, browser/runtime instances, IPC, endpoints, storage, and native add-ons. This shipped domain contract performs no extraction or I/O by itself.
|
||||
- Reconstruct static package, entrypoint, Webpack/Rspack module, import, worker, endpoint, storage, source-map, BrowserWindow, preload, contextBridge, IPC, utility-process, and native-add-on structure from a selected local directory or ASAR through `analyze_javascript_application` or `rea analyze-javascript-application`. Results and Evidence context are returned inline. The AST-only [application service](docs/javascript-artifact-reconstruction.md) never executes bootstrap code, pairs only unique exact literal IPC channels, and reports dynamic or ambiguous channels as unresolved.
|
||||
- Reconcile that static graph with existing passive web or Electron Evidence through `reconcile_javascript_runtime` or `rea reconcile-javascript-runtime`. Exact captured bytes outrank caller-declared file/URL mappings; target, frame, script, worker, cache, and asset ambiguity stays explicit, source-map authority stays separate, and a module resident in an observed bundle is never reported as executed. See [JavaScript static/runtime reconciliation](docs/javascript-runtime-reconciliation.md).
|
||||
- Trace a literal route, string, API, IPC channel, module, or native export through the complete reachable graph in authenticated application Evidence, then hand exact native artifact digests and requested exports to retained Ghidra or Hopper Evidence without automatic provider switching. Compare application versions using unique-only digest, source-map, structural, and semantic tiers; map a committed historical source inventory to bundle nodes with explicit digest and path scores; compare one exact JavaScript export's static return shapes through unique literal discriminants and JSON Pointer changes. Duplicate, dynamic, incomplete, ambiguous, and truncated facts stay unknown. See [cross-layer JavaScript application workflows](docs/javascript-application-workflows.md).
|
||||
- Classify PE/CLI managed artifacts with `inspect_managed_artifact` / `rea inspect-managed-artifact`, inspect file-backed metadata members, signatures, raw CIL hashes, decoded-instruction-tuple fingerprints, separately reported exception regions, call edges, and field-access anchors with `inspect_managed_members` / `rea inspect-managed-members`, inventory declared ModuleRef/ImplMap/PInvoke and non-IL method boundary indicators with `inspect_managed_native_boundaries` / `rea inspect-managed-native-boundaries`, then compare two authenticated member observations with `compare_managed_members` / `rea compare-managed-members`. `verify_managed_native_boundaries` / `rea verify-managed-native-boundaries` checks managed P/Invoke declarations against authenticated native export or function Evidence while keeping verified, inferred, contradicted, and unresolved states distinct. The comparison treats build-local tokens as build-local and uses unique decoded-CIL/signature and structural method-shape tiers, never names alone; tuple fingerprints do not themselves resolve tokens or fully commit control flow. `project_managed_application_graph` / `rea project-managed-application-graph` projects authenticated managed artifact/member/native-boundary Evidence into the existing application graph for cross-layer feature tracing. `import_managed_reconstruction` / `rea import-managed-reconstruction` admits user-supplied decompiler C#/IL/pseudocode as analyst inference only after exact artifact SHA-256, MVID, signature, and decoded-IL commitments match. Separately, `plan_managed_runtime_correlation` / `rea plan-managed-runtime-correlation` can admit a default-disabled, permission-gated runtime-correlation plan locked to the same build evidence. These paths never load the assembly, resolve CLR dependencies, execute target code, run a decompiler, or translate managed tokens into native addresses; complete normalized-CIL semantics, native-body bridge mapping, and an actual runtime executor remain future managed-code contracts.
|
||||
- Configure `REA_ILSPY_CMD_PATH=/absolute/path/to/ilspycmd` only when you want
|
||||
doctor and `verify:managed` to inspect a bring-your-own ILSpy command as a
|
||||
real reconstruction oracle. REA does not install ILSpy and does not treat
|
||||
decompiler text as canonical metadata or CIL observation.
|
||||
- Traverse content-addressed artifact graphs without extraction; on macOS, read-only DMG traversal additionally requires `native_mount_approved: true` and `REA_ARTIFACT_NATIVE_MOUNT_ENABLED=true`. Materialize only approved occurrences into absent output roots.
|
||||
- Build function dossiers with pseudocode, assembly, CFG edges, comments, calls, references, strings, and names.
|
||||
- Search and trace features across symbols, strings, metadata, references, and call paths.
|
||||
- Record every successful result as deterministic Evidence with artifact and provider identity, confidence, authority, limitations, and locations.
|
||||
- Export and import evidence bundles across sessions.
|
||||
- Capture approved PTY scenarios as Process Capture Evidence, including committed run manifests, raw and rendered terminal frames, scripted interactions, descendant settlement, named filesystem checkpoints, deterministic command shims, and loopback HTTP/WebSocket exchanges.
|
||||
- Validate finite replay machines without launching a target through `run_replay_machine` or `rea run-replay-machine`; ordered events return typed decisions, actions, captured aliases, transition journals, final state, and exact limit use without echoing request or captured values.
|
||||
- Compare complete artifact inventories by stable path, content, metadata, and relations; incomplete evidence never implies equivalence.
|
||||
- Compare explicit function dossiers across text, calls, references, strings, and address-normalized CFG topology with per-facet unknowns.
|
||||
- Compare canonical Evidence bundles by exact membership, explicit observation pairs, and residual-unknown histories without turning omissions into behavioral absence.
|
||||
- Aggregate runtime comparisons into observed behavior changes while keeping static artifact/function differences labeled as candidates.
|
||||
- Build Evidence-cited direct call paths by exact address without treating missing dossiers as graph leaves.
|
||||
- Correlate exact static/runtime findings through explicit hypotheses without claiming causality from cochange.
|
||||
- Verify finite behavioral and structural reconstruction specifications with pass, fail, and unknown kept distinct.
|
||||
- Track residual unknowns through immutable CAS revisions, evidence-qualified resolution, contradictions, probes, and validated dependency relationships.
|
||||
- Evidence-producing workflows record returned residual uncertainty as residual unknowns linked to their result Evidence. Errors without supporting Evidence do not create registry records.
|
||||
- Start six [guided MCP workflows](docs/mcp-prompts.md) with live, session-aware completion for documents, procedures, providers, evidence, captures, artifact IDs, and active unknowns.
|
||||
- **Native binaries:** Open Mach-O, ELF, PE, and Mac `.app` targets through Hopper or Ghidra. Inspect functions, strings, assembly, decompilation, calls, and references. 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.
|
||||
- **JavaScript and Electron:** Map modules, imports, source maps, routes, IPC channels, storage, and native add-ons without running the app. Compare builds and trace a feature across the recovered graph. Dynamic and ambiguous relationships remain unresolved. See [JavaScript application workflows](docs/javascript-application-workflows.md).
|
||||
- **Websites:** Inspect a selected page in an existing Chrome-family browser. Capture page structure, network metadata, script evidence, and screenshots requested by the call. Passive observation does not navigate or execute page JavaScript. See [browser observation](docs/browser-observation.md).
|
||||
- **Electron and Node runtime observation:** Inspect selected Electron pages or attach to a Node/Electron V8 Inspector target. Inspector observation records script locations and execution-context events; it does not infer imports, IPC activity, or which modules executed. See [runtime observation](docs/javascript-runtime-observation.md).
|
||||
- **.NET assemblies:** Inspect metadata and CIL instructions, compare builds, and check declared native dependencies without loading or running the assembly. Imported decompiler output is labeled as analyst inference. See [managed-code analysis](docs/managed-code-analysis.md).
|
||||
- **Controlled behavior capture:** Run process, browser, or Electron scenarios with the target, actions, and lifecycle declared in each request, then compare the resulting evidence. Missing observations cannot establish that two runs behaved the same way.
|
||||
- **Evidence and comparison:** Save results with artifact identity, provider, locations, confidence, and limitations. Export or import bundles, compare artifacts and functions, and connect static findings to runtime observations without claiming causality from correlation.
|
||||
- **Open questions:** Track unresolved findings, contradictions, and follow-up probes. Reconstruction checks report pass, fail, or unknown rather than treating missing evidence as a pass.
|
||||
- **Guided workflows:** Start six [MCP investigation workflows](docs/mcp-prompts.md) with suggestions based on your current session.
|
||||
|
||||
Hopper is the first provider, not the boundary of the project. Some current workflows still require Hopper and macOS; every evidence record identifies the provider and limitations behind its result.
|
||||
Windows Ghidra P0 supports the read-only native x86-64 PE boundary on local NTFS. Hopper-only features, such as GUI controls and annotations, are not available through Ghidra.
|
||||
|
||||
### Website observation with CDP
|
||||
|
||||
REA can inspect an already-running Chrome-family browser that you own. Browser observation is disabled by default and requires a literal loopback CDP endpoint plus exact approved page origins:
|
||||
REA can inspect an already-running Chrome-family browser through a literal loopback CDP endpoint. Each request names the endpoint and target; an optional origin filter can narrow discovery:
|
||||
|
||||
```bash
|
||||
export REA_BROWSER_OBSERVE_ENABLED=true
|
||||
export REA_BROWSER_CDP_ENDPOINTS_JSON='["http://127.0.0.1:9222"]'
|
||||
export REA_BROWSER_ALLOWED_ORIGINS_JSON='["http://127.0.0.1:3000"]'
|
||||
|
||||
rea list-browser-targets http://127.0.0.1:9222 --approved --json
|
||||
rea inspect-web-page http://127.0.0.1:9222 TARGET_ID --approved --json
|
||||
rea list-browser-targets http://127.0.0.1:9222 --json
|
||||
rea inspect-web-page http://127.0.0.1:9222 TARGET_ID --json
|
||||
```
|
||||
|
||||
All eight browser tools expose the same Evidence contracts over CLI and MCP. Inspection is passive: REA does not evaluate page JavaScript, navigate, click, close the page, or close the browser. Query values, credentials, cookies, authorization headers, storage values, and raw JSON or WebSocket values are never retained. Separately approved captures can retain bounded redacted console primitives, value-free JSON/WebSocket shapes, script sources, accessibility text, or screenshot pixels. Existing activity before attach is explicitly unavailable. See [Website observation with CDP](docs/browser-observation.md) for browser startup, schemas, limits, and the threat model.
|
||||
The eight passive browser tools work through both CLI and MCP. They inspect the selected page without navigating, clicking, or evaluating its JavaScript. Credentials, cookies, authorization headers, and raw payload values are not retained. A request selects whether to include script sources, accessibility text, screenshots, or console and payload summaries. REA cannot observe activity that happened before it attached. See [browser observation](docs/browser-observation.md) for browser startup, capture options, and limits.
|
||||
|
||||
### Controlled browser scenarios
|
||||
|
||||
`capture_browser_scenario` is a separate, explicitly mutating browser boundary.
|
||||
It runs only the fixed, versioned scenario vocabulary through Playwright and
|
||||
returns step-indexed Evidence for screenshots, DOM, accessibility, URL/history,
|
||||
storage, console/errors, network, WebSockets, frames, workers, popups, and
|
||||
cancelled downloads. Missing or truncated sections can never support equality
|
||||
claims.
|
||||
`capture_browser_scenario` runs a caller-declared sequence of browser actions through
|
||||
Playwright. Unlike passive observation, it can interact with the page. Each
|
||||
step records evidence such as screenshots, page structure, navigation, and
|
||||
network activity. Missing or truncated observations cannot establish that two
|
||||
runs behaved the same way.
|
||||
|
||||
```bash
|
||||
export REA_BROWSER_SCENARIO_ENABLED=true
|
||||
export REA_BROWSER_SCENARIO_EXECUTABLE_ROOTS_JSON='["/usr/bin"]'
|
||||
export REA_BROWSER_SCENARIO_CDP_ENDPOINTS_JSON='["http://127.0.0.1:9222"]'
|
||||
export REA_BROWSER_SCENARIO_ALLOWED_ORIGINS_JSON='["http://127.0.0.1:3000"]'
|
||||
export REA_BROWSER_SCENARIO_ALLOWED_ENV_JSON='["REA_TEST_PASSWORD"]'
|
||||
|
||||
rea capture-browser-scenario ./scenario.json --json
|
||||
```
|
||||
|
||||
Launch mode owns a temporary browser profile and removes it after terminating
|
||||
the launched browser. Connect mode accepts one exact loopback CDP target and
|
||||
disconnects without closing the external browser. Automation has no default
|
||||
grant: use the shared project/session policy, or set
|
||||
`REA_BROWSER_SCENARIO_AUTO_GRANT=true` only for a trusted unattended
|
||||
environment. Scenario JSON contains secret references and environment-variable
|
||||
names, never secret values. See the
|
||||
disconnects without closing the external browser. The request supplies the
|
||||
selected executable or endpoint, actions, and any origin or environment
|
||||
selections needed by the scenario. Scenario JSON contains secret references and
|
||||
environment-variable names, never secret values. See the
|
||||
[browser scenario contract](docs/browser-scenario-contract.md).
|
||||
|
||||
### Node and Electron V8 Inspector observation
|
||||
|
||||
Attach-only JavaScript runtime observation is separately disabled by default:
|
||||
Node and Electron runtime observation attaches to an existing Inspector target named in the request:
|
||||
|
||||
```bash
|
||||
export REA_V8_INSPECTOR_OBSERVE_ENABLED=true
|
||||
|
||||
rea list-javascript-runtime-targets http://127.0.0.1:9229 --json
|
||||
rea observe-javascript-runtime http://127.0.0.1:9229 TARGET_ID \
|
||||
--runtime-kind node --json
|
||||
```
|
||||
|
||||
REA sends only `Runtime.enable` and `Debugger.enable`. It retains validated
|
||||
script locations and execution-context lifecycle events;
|
||||
require/import edges, EventEmitter activity, Electron IPC, PID identity, and
|
||||
Electron role identity stay explicit unknowns. See
|
||||
[passive Node and Electron runtime observation](docs/javascript-runtime-observation.md).
|
||||
REA records script locations and execution-context events without evaluating
|
||||
code or setting breakpoints. These observations do not establish import
|
||||
relationships, event activity, IPC, process identity, or Electron roles. See
|
||||
[Node and Electron runtime observation](docs/javascript-runtime-observation.md)
|
||||
for the exact coverage.
|
||||
|
||||
Exact package, tool-family, provider, setup-client, schema, and CLI facts are generated from source in [`docs/product-catalog.json`](docs/product-catalog.json). PR CI verifies this catalog, narrative documentation, generated schemas, and a clean TypeDoc render.
|
||||
Exact package, tool-family, provider, setup-client, schema, and CLI facts are generated from source in [`docs/product-catalog.json`](docs/product-catalog.json). PR CI verifies this catalog, narrative documentation, and generated schemas.
|
||||
|
||||
## Roadmap
|
||||
|
||||
REA is growing into a toolkit for understanding software across static artifacts and observed behavior. The [current status](#current-status) above is the shipped baseline; the items below are planned work.
|
||||
The [current status](#current-status) section describes shipped capabilities. These are the next areas of work.
|
||||
|
||||
### Now
|
||||
|
||||
1. **Maintain truthful product metadata** — extend the shipped canonical catalog and drift checks whenever versions, tools, providers, schemas, setup clients, or CLI capabilities change.
|
||||
2. **Cross-provider conformance growth** — add source-owned architectures and difficult indirect/thunk cases while preserving semantic comparison and provider-specific text boundaries.
|
||||
1. **Keep documentation accurate:** update the generated catalog and documentation checks when tools, providers, setup options, or versions change.
|
||||
2. **Test more native binaries:** expand architecture and indirect-call coverage across Hopper and Ghidra.
|
||||
|
||||
### Next
|
||||
|
||||
1. **Controlled replay conformance growth** — extend the shipped Linux extracted-module sandbox with more source-owned hostile fixtures and cross-kernel conformance; browser and Electron scenarios remain separately permissioned authorities.
|
||||
2. **Broader application graph evidence** — extend authenticated cross-layer traces with additional static extractors and additional runtime authorities.
|
||||
3. **Professional managed-code analysis** — extend shipped PE/CLI triage, CIL evidence, managed/native declaration inventory, source-owned conformance, and obfuscation-resistant comparisons toward verified native-provider composition under the accepted [managed-code boundary](docs/managed-code-analysis.md).
|
||||
4. **Deterministic behavior harnesses** — extend process ownership, protocol fixtures, filesystem observation, reconnects, and cross-version behavioral comparison.
|
||||
1. **Connect more application layers:** add static extractors and runtime observations to feature traces.
|
||||
2. **Extend .NET analysis:** improve comparisons of obfuscated assemblies and connect managed findings to verified native analysis. See the [managed-code guide](docs/managed-code-analysis.md).
|
||||
3. **Compare more runtime behavior:** expand process, protocol, filesystem, reconnect, and version-comparison coverage.
|
||||
|
||||
### Later
|
||||
|
||||
1. **Broader controlled application interaction** — extend the separately authorized browser and Electron scenario surfaces beyond the current bounded click/wait actions without widening passive observation or extracted-module replay authority.
|
||||
2. **Native runtime observation** — approval-gated LLDB, Frida, system logs, process/filesystem observers, and native API tracing.
|
||||
3. **Additional providers and targets** — evaluate IDA/Hex-Rays, Binary Ninja, Rizin, LIEF, Windows-native providers, mobile artifacts, firmware, document formats, and other software-defined systems.
|
||||
1. **Expand browser and Electron interaction:** add scenario actions beyond the current click and wait operations.
|
||||
2. **Observe native apps at runtime:** explore LLDB, Frida, system logs, and native API tracing.
|
||||
3. **Evaluate more tools and targets:** assess IDA/Hex-Rays, Binary Ninja, Rizin, LIEF, Windows-native tools, mobile apps, and firmware.
|
||||
|
||||
New providers must produce the same evidence and safety metadata as existing capabilities before they become part of the public workflow. Once REA has multiple optional toolchains, setup can become capability-selective; the consent rules for that future work are recorded in the [installation roadmap](docs/roadmap.md).
|
||||
Setup already lets you choose agent integration and Hopper installation. Support for installing additional analysis tools is future work, described in the [installation roadmap](docs/roadmap.md).
|
||||
|
||||
See the [static-analysis provider evaluation](docs/provider-evaluation.md) for the shipped Ghidra function-analysis boundary, remaining admission gates, and provider comparison matrix; [ADR-0001](docs/adr/0001-provider-selection-and-analysis-profiles.md) for binding, selection, profile, snapshot, and compatibility decisions; the [controlled replay guide](docs/controlled-javascript-replay.md) plus [ADR-0002](docs/adr/0002-controlled-replay-authority-and-sandbox.md) for the shipped JavaScript replay boundary; and [ADR-0003](docs/adr/0003-managed-code-evidence-and-provider-boundary.md) for the managed-code evidence and provider design.
|
||||
|
||||
See the [native UI and dispatch investigation guide](docs/native-investigation.md) for compiled Interface Builder decoding, symbol-derived metadata limits, and the current p-code value-flow boundary.
|
||||
See [provider evaluation](docs/provider-evaluation.md) for coverage and remaining requirements, and the [native investigation guide](docs/native-investigation.md) for UI, dispatch, and value-flow analysis.
|
||||
|
||||
## Using REA with other agents
|
||||
|
||||
Setup detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, and Devin. It automatically configures the first six when present; detected Devin installations are reported but left unchanged. Any agent that supports local MCP servers can use REA with the configuration below.
|
||||
Setup offers supported agent integrations for selection. Existing REA registrations are selected by default; newly detected agents remain unselected until chosen. Any agent that supports local MCP servers can use the configuration below.
|
||||
|
||||
### Manual MCP configuration
|
||||
|
||||
@@ -515,7 +424,7 @@ Setup detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf,
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@3.2.1", "mcp"]
|
||||
"args": ["-y", "rea-agents@4.0.1", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -524,11 +433,11 @@ Setup detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf,
|
||||
<!-- x-release-please-end -->
|
||||
|
||||
Persistent registrations should use one exact package version. `rea setup`
|
||||
maintains that pin, upgrades the bundled skill at the same time, and gives Codex
|
||||
a 30-second startup allowance for a cold package-runner start. An interactive
|
||||
`rea upgrade` opens the updated setup plan after installing the new executable;
|
||||
structured or non-interactive upgrades tell you to run that sync explicitly.
|
||||
Restart clients whose approved registration changed.
|
||||
maintains that pin, updates the bundled skill at the same time, and gives Codex
|
||||
a 30-second startup allowance for a cold package-runner start. `rea update`
|
||||
installs an exact release and verifies the new executable. It returns an
|
||||
unapplied maintenance plan for existing REA integrations, with a scoped setup
|
||||
command to review and approve their changes. Restart affected agents afterward.
|
||||
|
||||
MCP clients that support prompts can also discover six ordered investigation
|
||||
workflows through `prompts/list`. Their optional identifier arguments use the
|
||||
@@ -544,7 +453,7 @@ flowchart LR
|
||||
REA --> Session["Target-bound session router"]
|
||||
Session --> Registry["Deep-provider registry<br/>deterministic selection"]
|
||||
Registry --> Hopper["Hopper provider"]
|
||||
Registry --> Ghidra["Ghidra provider<br/>read-only inventory + function analysis"]
|
||||
Registry --> Ghidra["Ghidra provider<br/>inventory + function analysis + annotations"]
|
||||
Hopper --> Runtime["Owned provider runtime<br/>deadline + bounded diagnostics + cleanup"]
|
||||
Ghidra --> Runtime
|
||||
Session --> Native["Native macOS provider"]
|
||||
@@ -584,7 +493,7 @@ Or install the `rea` command globally:
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea --help
|
||||
rea upgrade
|
||||
rea update
|
||||
rea mcp
|
||||
```
|
||||
|
||||
@@ -592,8 +501,7 @@ REA accepts a Mac `.app` folder directly. If an agent cannot find an app by name
|
||||
|
||||
### Choosing a deep-analysis provider
|
||||
|
||||
Every deep-analysis open resolves a provider before creating its client. The
|
||||
same selector and precedence apply to the CLI, MCP, and startup configuration:
|
||||
Choose which analysis tool to use from the CLI:
|
||||
|
||||
```bash
|
||||
rea providers --json
|
||||
@@ -610,47 +518,29 @@ For MCP, pass the optional selector on `open_binary`:
|
||||
}
|
||||
```
|
||||
|
||||
The request-level `provider_id` or `--provider` wins over
|
||||
`REA_ANALYSIS_PROVIDER`; all accept a provider ID or `auto`. Automatic selection
|
||||
binds the sole usable deep candidate, reports `ambiguous` when several are
|
||||
usable, and can leave an artifact-only target unbound so its disjoint artifact
|
||||
operations still work. An explicit unknown, unavailable, or unsupported
|
||||
provider fails with candidate IDs, stable rejection codes, and actionable local
|
||||
diagnostics. `binary_session`, `rea providers`, and `rea capabilities` expose
|
||||
the authoritative `analysis_provider_candidates` and
|
||||
`analysis_provider_binding` fields. Each open target also has an
|
||||
`analysis_run.run_id` allocated before provider startup. When dynamic providers
|
||||
start, `analysis_run.process_lineage` changes from `not_observed` to `snapshots`
|
||||
and retains one provider-attributed, token-verified observation per started
|
||||
provider. Each observation carries `observed_at` and remains `unavailable` or
|
||||
`verified`; a verified empty descendant list is distinct from both. Snapshots
|
||||
describe bounded observations, not current live state or historical absence.
|
||||
For providers with serial work, `analysis_activity` distinguishes `idle`,
|
||||
`busy`, and `timed_out_busy`, and reports the active operation, elapsed time,
|
||||
caller state, timeout, and queued-request count. A caller timeout therefore
|
||||
does not falsely imply that Hopper's Python thread is available. `close_binary`
|
||||
clears the REA session but returns `cleanup_incomplete` when authenticated
|
||||
document shutdown, owned process cleanup, or private runtime removal cannot be
|
||||
verified.
|
||||
Reopening same target without a selector keeps its binding; runtime failure
|
||||
never selects another provider silently.
|
||||
Ghidra can appear as an available, target-compatible candidate after doctor
|
||||
validates its exact installation. Its capability list contains the 19 admitted
|
||||
read-only inventory and function-analysis operations; selecting it still does
|
||||
not make Hopper-only GUI or mutation operations available and never triggers a
|
||||
silent fallback.
|
||||
Use `--provider`, or `provider_id` in MCP, to choose Hopper or Ghidra for a target. This choice overrides `REA_ANALYSIS_PROVIDER`.
|
||||
|
||||
With `auto`, REA selects the only available tool that supports the target. If both are available, specify one before opening the target. The session keeps that choice until you explicitly switch or close it; a failure never silently switches tools. Artifact-only analysis can work without a native analysis tool.
|
||||
|
||||
Run `rea providers` and `rea capabilities` to check availability and supported operations. Ghidra supports inspection and atomic function annotation edits on Linux and macOS. Its database edits are discarded on close; GUI controls require Hopper. Windows Ghidra P0 supports read-only analysis of native x86-64 PE applications on local NTFS with bundled native controls.
|
||||
|
||||
The session also reports active work and cleanup status. If a caller times out, the analysis tool may still be busy; `analysis_activity` reports that state. A `cleanup_incomplete` result identifies resources whose shutdown or removal could not be verified. See [provider selection and analysis profiles](docs/adr/0001-provider-selection-and-analysis-profiles.md) for session, cache, and process-tracking details.
|
||||
|
||||
### CLI exit status
|
||||
|
||||
| Status | Meaning |
|
||||
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `0` | The requested operation completed. Truthful unknowns, warnings, and partial or truncated evidence remain successful results. |
|
||||
| `1` | Arguments, policy, permission, provider analysis, integrity checking, cancellation, timeout, setup, diagnostics, update, uninstall, output encoding, or output writing prevented completion. Structured output identifies the failure category when REA could encode it. |
|
||||
| `128 + N` | The process ended from signal `N`, where the shell or runtime preserves the conventional signal-derived status. |
|
||||
| Status | Meaning |
|
||||
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `0` | The operation completed. Results may still include warnings, partial evidence, or unresolved questions. |
|
||||
| `1` | The operation could not complete, for example because of invalid input, host permission denial, cancellation, or timeout. Structured output reports the reason when available. |
|
||||
| `128 + N` | The process ended from signal `N`, where the shell or runtime preserves the conventional signal-derived status. |
|
||||
|
||||
`setup` returns `1` for `planned`, `needs_confirmation`, or `needs_human`
|
||||
`setup --dry-run` returns status `planned` and exits `0`. A setup result with
|
||||
status `cancelled` also exits `0`.
|
||||
`setup` returns `1` for `needs_confirmation` or `needs_human`
|
||||
because configuration is not ready; rerun it after approval or remediation.
|
||||
`doctor` returns `1` when any check is unhealthy. Output format, full envelopes,
|
||||
`doctor` returns `1` when required checks for its readiness scope are unhealthy.
|
||||
Unavailable optional provider prerequisites remain visible as informational
|
||||
diagnostics and do not block setup or unrelated providers. Output format, full envelopes,
|
||||
filters, and token controls never change the operation status.
|
||||
|
||||
When REA feeds a shell pipeline, enable `pipefail` so a downstream formatter
|
||||
@@ -658,33 +548,25 @@ cannot hide its failure:
|
||||
|
||||
```bash
|
||||
set -o pipefail
|
||||
rea inventory-artifact ./app.asar --json | jq . > inventory.json
|
||||
rea inspect-artifact ./app.asar --json | jq . > inspection.json
|
||||
```
|
||||
|
||||
## Current Hopper provider
|
||||
|
||||
REA starts Hopper when needed; Hopper does not need to be running first. Hopper's launcher internally activates the application, so opening a target may bring Hopper to the foreground. REA asks macOS to start Hopper hidden and in the background when possible, but cannot guarantee that it will remain behind the current application.
|
||||
REA starts Hopper when an operation needs it. On macOS, Hopper may bring its window or a dialog to the foreground even though REA requests background startup. Demo or license prompts may need your attention.
|
||||
|
||||
REA derives explicit format and architecture arguments to prevent common FAT and ARM selection dialogs. Other Hopper or macOS dialogs may still require a person. REA reports startup failures and remediation through CLI or MCP results instead of attempting to answer UI prompts.
|
||||
Hopper handles one analysis request at a time. Cancelling your wait does not stop work already running inside Hopper; the session reports whether it is still busy. Successful decompilation results are cached until a relevant rename or comment change.
|
||||
|
||||
Hopper bridge calls pass through a serial FIFO because Hopper's Python API runs on one dedicated thread. Calls wait for a response or caller cancellation. Cancelling an active call settles that caller but keeps its wire slot until Hopper replies, preserving request/response correlation; `binary_session.analysis_activity` exposes active work and MCP progress reports elapsed time. Successful decompilation text is cached per document and procedure, shared by pseudocode and dossier requests, and invalidated after rename or comment mutation.
|
||||
Use `rea instructions` when you only need assembly instructions for a function. It avoids decompilation and a whole-program inventory.
|
||||
|
||||
Hopper's synchronous public Python calls return when the operation completes; callers may cancel their wait. Use `read_function_instructions` or `rea instructions` when raw instruction orientation answers the question, without requesting a decompilation or whole-program name/string inventory.
|
||||
Closing a session shuts down REA's bridge and removes its temporary socket directory while preserving a Hopper application you may be using. If cleanup cannot be verified, `close_binary` reports `cleanup_incomplete` and the affected resources.
|
||||
|
||||
Closing a REA session shuts down its bridge and removes its private socket directory. It does not quit a Hopper application the user may be using. If shutdown or cleanup cannot be verified, `close_binary` returns `cleanup_incomplete` with the affected local resources.
|
||||
## Process capture
|
||||
|
||||
## Advanced process-capture setup
|
||||
|
||||
Process capture is disabled by default. Enabling it requires
|
||||
`REA_PROCESS_CAPTURE_ENABLED=true`, approved executable and working roots in
|
||||
`REA_PROCESS_EXECUTABLE_ROOTS_JSON` and `REA_PROCESS_WORKING_ROOTS_JSON`, and an
|
||||
environment allowlist in `REA_PROCESS_ALLOWED_ENV_JSON`. Because the current PTY
|
||||
adapter uses host networking, it also requires
|
||||
`REA_PROCESS_ALLOW_EXTERNAL_NETWORK=true`.
|
||||
|
||||
Set `REA_PROCESS_CAPTURE_AUTO_GRANT=false` to configure those process-capture
|
||||
limits as a ceiling without implicitly granting them. This mode remains
|
||||
fail-closed until a narrower grant is established.
|
||||
Process capture runs the exact executable and scenario declared in the request,
|
||||
with the requested working directory and environment. Filesystem observation
|
||||
paths select what to snapshot. The process runs with your user permissions;
|
||||
Process Capture records behavior and is not a security sandbox.
|
||||
|
||||
Capture a scenario or compare two saved Process Capture Evidence records:
|
||||
|
||||
@@ -695,9 +577,9 @@ rea compare-process-captures authority.json reconstruction.json
|
||||
```
|
||||
|
||||
The comparison reports each observed dimension separately and identifies the
|
||||
first terminal, interaction, exit, filesystem, protocol, process, or shim
|
||||
divergence. See [Process Capture](docs/process-capture.md) for scenario
|
||||
fields, command-shim replay, checkpoint triggers, limits, and safety behavior.
|
||||
first terminal, interaction, exit, filesystem, or process divergence.
|
||||
See [Process Capture](docs/process-capture.md) for scenario fields, limits, and
|
||||
evidence boundaries.
|
||||
|
||||
REA installs a prebuilt PTY backend for supported macOS, Linux, and Windows
|
||||
architectures. If the capability check reports that the backend is unavailable,
|
||||
@@ -714,7 +596,11 @@ verified or absent.
|
||||
|
||||
## Security model
|
||||
|
||||
REA does not provide a hosted analysis service. Hopper and Linux Ghidra bridge communication uses authenticated private local sockets. Windows Ghidra P0 uses authenticated IPv4 loopback but does not claim named-pipe DACL or hostile-local-user isolation. Dynamic capabilities are disabled by default and require both operator policy and explicit per-call approval. Shipped providers, passive observers, and Process Capture are not security sandboxes: providers and launched targets run with the current user's permissions. Extracted JavaScript replay is a distinct Linux-only capability that fails closed unless Bubblewrap namespaces, architecture-checked seccomp, private runtime mounts, and delegated cgroup limits are available; it never inherits browser, Electron, or Process Capture authority. See [ADR-0002](docs/adr/0002-controlled-replay-authority-and-sandbox.md). Report vulnerabilities through the private process in [SECURITY.md](SECURITY.md).
|
||||
Analysis runs locally. REA communicates with Hopper and Ghidra through authenticated private local sockets. Your agent or model provider has its own data policy.
|
||||
|
||||
Runtime requests act on the declared target and lifecycle. Analysis tools and launched targets run with your user permissions, and native UI capture still depends on macOS Accessibility and Screen Recording access. Static JavaScript analysis does not execute extracted modules; use direct browser, Electron, or process capture when runtime behavior is needed.
|
||||
|
||||
Windows Ghidra P0 automatically uses native Job Objects, protected private-runtime DACLs, and handle-based admission. Its experimental scope is native x86-64 PE applications on local NTFS. Report vulnerabilities through the private process in [SECURITY.md](SECURITY.md).
|
||||
|
||||
## FAQ
|
||||
|
||||
@@ -728,7 +614,7 @@ No. REA starts Hopper when an operation needs it. An already-running Hopper appl
|
||||
<details>
|
||||
<summary><strong>Why did Hopper appear in front of my other windows?</strong></summary>
|
||||
|
||||
Hopper's launcher internally activates the application. REA requests background startup, but macOS and Hopper may still bring a window or dialog forward. See [Hopper application behavior](#hopper-application-behavior).
|
||||
Hopper's launcher internally activates the application. REA requests background startup, but macOS and Hopper may still bring a window or dialog forward. See [Current Hopper provider](#current-hopper-provider).
|
||||
|
||||
</details>
|
||||
|
||||
@@ -742,7 +628,7 @@ No. Setup can install Hopper for you, but Hopper remains separate software with
|
||||
<details>
|
||||
<summary><strong>Does REA install or include Ghidra or Java?</strong></summary>
|
||||
|
||||
No. Ghidra support is bring-your-own. REA packages only its Java bridge source, validates the exact supported Ghidra 12.1.4 and 64-bit JDK 21 installation, and loads that bridge through Ghidra's external script path after setup approval records the paths.
|
||||
No. REA connects to an existing Ghidra installation. Run setup after providing the Ghidra and Java paths shown in the [Ghidra section](#ghidra-analysis-provider).
|
||||
|
||||
</details>
|
||||
|
||||
@@ -763,48 +649,17 @@ No decompiler can guarantee the original source. REA gives an agent pseudocode,
|
||||
<details>
|
||||
<summary><strong>Which agents can use REA?</strong></summary>
|
||||
|
||||
Any agent that can run a local MCP server can use the manual configuration. Setup detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, and Devin; it automatically configures the first six when present and reports Devin without modifying it.
|
||||
Any agent that can run a local MCP server can use the manual configuration. Setup offers the supported integrations listed in [Installation and setup](docs/installation.md#supported-agents); existing REA registrations are selected by default, and other detected agents require selection.
|
||||
|
||||
</details>
|
||||
|
||||
## Development
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, architecture, and release
|
||||
instructions, and [docs/testing.md](docs/testing.md) for the behavioral test
|
||||
depths, focused developer commands, coverage floors, and CI evidence. PR CI
|
||||
publishes generated API documentation as the `api-docs` workflow artifact.
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and contribution checks, and [docs/testing.md](docs/testing.md) for test scopes and real-tool verification. `npm run docs:check` checks committed generated documents; `npm run docs:generate` regenerates them.
|
||||
|
||||
`npm run verify:agent` runs brandless native, JavaScript-application, managed,
|
||||
and browser prompts through a real local Codex CLI. Its JSON report measures
|
||||
natural MCP use, first-tool routing, repeated calls, actual Codex token usage,
|
||||
completion quality, and explicit treatment of authority and unknowns.
|
||||
`npm run verify:agent` evaluates native, JavaScript, managed, and browser investigation tasks through a real local Codex CLI. Its report covers tool selection, repeated calls, token use, completion quality, and handling of permissions and unknowns.
|
||||
|
||||
`npm run evidence:generate` regenerates the managed conformance manifest and
|
||||
Evidence completion ledger from live verifier output. `npm run
|
||||
evidence:check` reruns the verifier and fails when artifacts, scenarios,
|
||||
providers, schemas, claim counts, Evidence IDs, or the bundled skill have
|
||||
drifted. Unsupported claims remain explicit and never count as passes.
|
||||
Verifier JSON reports include an ephemeral `verifier_run` UUID allocated before
|
||||
the verifier performs work and inherited by its child processes through
|
||||
`REA_PROCESS_RUN_ID`. The final report includes the verifier and parent PIDs
|
||||
plus `process_lineage` and its ISO `observed_at` timestamp: POSIX verifiers
|
||||
report a token-verified, point-in-time launcher process group and live
|
||||
descendants, while platforms without an owned
|
||||
lineage primitive report `status: "unavailable"` and a reason. An empty verified
|
||||
descendant list means no child was live during the final observation; it does
|
||||
not claim the verifier launched no children earlier. Nested verifier entrypoints
|
||||
in the same process reuse its UUID; each new verifier process replaces any
|
||||
inherited parent token with a fresh run identity.
|
||||
Generated completion commitments deliberately exclude this per-execution
|
||||
identity, so check mode remains deterministic while live reports remain
|
||||
attributable.
|
||||
|
||||
Owned Hopper shutdown logs retain the launcher PID, process-group ID, cleanup
|
||||
status, and whether a verified group signal was required. They omit the run
|
||||
token and unexpected exception text. The real-Hopper verifier also starts an
|
||||
unrelated `Hopper`-named sentinel in a distinct process group and fails unless
|
||||
that process survives every session close; the sentinel is removed only by the
|
||||
verifier after the survival check.
|
||||
`npm run evidence:generate` regenerates the managed conformance manifest and Evidence completion ledger from live verification results. `npm run evidence:check` reruns verification and checks for drift. Unsupported claims remain explicit and do not count as passes. See [testing](docs/testing.md) for verification commands and process-cleanup checks.
|
||||
|
||||
## Project links
|
||||
|
||||
|
||||
+126
-85
@@ -4,7 +4,7 @@
|
||||
|
||||
# REA: هندسة أي شيء عكسيًا
|
||||
|
||||
### واجهة CLI وخادم MCP واحدان يمكّنان وكلاء البرمجة من إجراء هندسة عكسية لأي شيء
|
||||
### استقصِ سلوك التطبيقات والملفات التنفيذية الأصلية باستخدام وكيلك.
|
||||
|
||||
**اعثر على ميزة تعجبك. افهم آلية عملها. ابنها بالطريقة التي تريدها.**
|
||||
|
||||
@@ -13,109 +13,152 @@
|
||||
[](#منصة-أدوات-التحقيق)
|
||||
[](https://nodejs.org/)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
[البدء السريع](#البدء-السريع) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [دليل الأدوات](#منصة-أدوات-التحقيق) · [كيف يعمل](#كيف-يعمل) · [الأسئلة الشائعة](#الأسئلة-الشائعة)
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/82054/weekly?language=TypeScript" alt="morluto/rea | Trendshift" width="250" height="55" /></a>
|
||||
|
||||
[البدء السريع](#البدء-السريع) · [حالة الدعم](#حالة-الدعم) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [منصة أدوات التحقيق](#منصة-أدوات-التحقيق) · [خطة العمل](#خطة-العمل) · [كيف يعمل؟](#كيف-يعمل)
|
||||
|
||||
<table aria-label="REA community">
|
||||
<tr>
|
||||
<td align="center" width="360">
|
||||
<a href="https://discord.gg/GkcryMnJDM">
|
||||
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
|
||||
<strong>انضم إلى مجتمع الهندسة العكسية</strong>
|
||||
</a><br />
|
||||
<sub>Discord · أسئلة وأجوبة · شارك أعمالك</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
<code>curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash</code>
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="REA يشغّل جسر التحليل داخل Hopper لفحص ملف تنفيذي أصلي" width="1200" />
|
||||
|
||||
</div>
|
||||
|
||||
هل وجدت في تطبيق ما ميزة تريدها في منتجك؟ أعط التطبيق إلى وكيل البرمجة حتى من دون شيفرته المصدرية. باستخدام REA، يستطيع الوكيل استقصاء الميزة وفهم آلية عملها ثم بناء نسخة ملائمة لتقنياتك وتصميمك ومتطلباتك.
|
||||
---
|
||||
|
||||
يوفر REA هذه العملية عبر CLI وخادم MCP واحدين. يستطيع الوكيل قراءة شيفرة التطبيق وتتبع طريقة عمل الميزات ثم استخدام الأدلة مباشرة في عمله البرمجي المعتاد. ويتولى REA أدوات الهندسة العكسية المعقدة خلف واجهة واحدة.
|
||||
هل وجدت في تطبيق ميزة تريدها في منتجك؟ اطلب من وكيلك استقصاءها باستخدام REA. يستطيع فحص التطبيق من دون شيفرته المصدرية، وشرح طريقة عمل الميزة وعرض الأدلة، ثم بناء نسخة تناسب مشروعك.
|
||||
|
||||
يوفر REA أدوات لتحليل الملفات التنفيذية الأصلية وتطبيقات JavaScript وElectron وتجميعات .NET والمواقع. يمكنك استخدامها من وكيلك أو من الطرفية. يعمل التحليل محليًا، وتتضمن النتائج الأدلة والقيود.
|
||||
|
||||
يضبط Setup إعدادات الوكيل ويربطه بتثبيت موجود من Hopper أو Ghidra. ويمكنه تثبيت Hopper بعد موافقتك.
|
||||
|
||||
## اطلب من وكيلك مباشرة
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
ثم اطلب:
|
||||
بعد [الإعداد](#البدء-السريع)، أعد تشغيل الوكيل ثم اطلب:
|
||||
|
||||
```text
|
||||
أعد REA وأجر هندسة عكسية لتطبيق Notes. اشرح طريقة عمل البحث وكيف توصلت إلى ذلك،
|
||||
ثم ابنِ ميزة مشابهة لمشروعي.
|
||||
استقصِ طريقة عمل البحث في تطبيق Notes، واعرض الأدلة، ثم ابنِ ميزة مشابهة لمشروعي.
|
||||
```
|
||||
|
||||
تطبيق Notes مجرد مثال. يمكنك تسمية أي تطبيق تريد فهمه أو طلب البدء بنظرة عامة.
|
||||
استبدل Notes بالتطبيق الذي تريد فهمه، أو اطلب نظرة عامة أولًا.
|
||||
|
||||
## من الملف التنفيذي إلى السلوك
|
||||
|
||||
| فك الترجمة | الفهم | إعادة الإنشاء |
|
||||
| ----------------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------- |
|
||||
| فك الترجمة | الفهم | إعادة الإنشاء |
|
||||
| ---------------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------ |
|
||||
| استخرج الشيفرة شبه المصدرية، والتعليمات، والسلاسل النصية، والرموز | تتبع تدفق التحكم والمراجع المتبادلة والاستدعاءات والأنواع | حوّل ما تعلمه الوكيل إلى ميزة تناسب تقنياتك وواجهتك ومتطلباتك |
|
||||
|
||||
يوضح REA كيف وصل إلى نتائجه. وهو لا يدّعي استعادة الشيفرة المصدرية الأصلية أو استنساخ التطبيق تلقائيًا.
|
||||
|
||||
## لماذا REA؟
|
||||
|
||||
| | |
|
||||
| -------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| **مصمم للوكلاء** | اسأل عما يفعله تطبيق مترجم ودع الوكيل يجمع الأدلة بدلًا من التخمين. |
|
||||
| **CLI وMCP** | استخدم قدرات الهندسة العكسية نفسها من الطرفية أو وكيل البرمجة. |
|
||||
| **يتولى التعقيد** | يدير REA إعداد الأدوات وفتح التطبيق واستمرار الاستقصاء والتنظيف بعد الانتهاء. |
|
||||
| | |
|
||||
| -------------------- | ------------------------------------------------------------------------------------- |
|
||||
| **مصمم للوكلاء** | اسأل عما يفعله تطبيق مترجم ودع الوكيل يجمع الأدلة بدلًا من التخمين. |
|
||||
| **CLI وMCP** | استخدم قدرات الهندسة العكسية نفسها من الطرفية أو وكيل البرمجة. |
|
||||
| **يتولى التعقيد** | يدير REA إعداد الأدوات وفتح التطبيق واستمرار الاستقصاء والتنظيف بعد الانتهاء. |
|
||||
| **سير عمل كامل** | انتقل من النظرة الأولى إلى الشيفرة شبه المصدرية وعلاقات الاستدعاء والأنواع وأدلة التنفيذ. |
|
||||
| **محلي حسب التصميم** | يجري التحليل على جهاز Mac ولا يرفع REA الملف التنفيذي إلى خدمة تحليل مستضافة. |
|
||||
| **يحافظ على السياق** | استقصِ عدة ملفات تنفيذية من دون بدء التحليل من جديد عند كل سؤال. |
|
||||
| **محلي حسب التصميم** | يجري التحليل على جهازك المدعوم ولا يرفع REA الملف التنفيذي إلى خدمة تحليل مستضافة. |
|
||||
| **يحافظ على السياق** | استقصِ عدة ملفات تنفيذية من دون بدء التحليل من جديد عند كل سؤال. |
|
||||
|
||||
## البدء السريع
|
||||
|
||||
### باستخدام وكيل برمجة — موصى به
|
||||
### تشغيل الإعداد (موصى به)
|
||||
|
||||
اربط REA بوكيلك:
|
||||
|
||||
```bash
|
||||
npx --yes rea-agents@latest setup
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
اطلب من الوكيل إعداد REA. سيفحص جهاز Mac ويشرح ما يحتاج إلى تثبيته ويطلب الموافقة ويرشدك خلال رسائل النظام. أعد تشغيل الوكيل بعد الإعداد إذا طلب ذلك لتحميل مجموعة الأدوات الكاملة.
|
||||
يعرض Setup أولاً قائمة متعددة الاختيار للوكلاء الذين تريد ربطهم. تكون تسجيلات REA الموجودة محددة افتراضياً، ولا يؤدي اكتشاف عميل جديد وحده إلى تحديده؛ ويمكنك اختيار عميل غير مهيأ. راجع المسارات والتغييرات المحددة قبل الموافقة. يثبت Setup سير عمل REA للوكلاء المحددين افتراضياً. Hopper خيار منفصل يتطلب موافقة مستقلة، ويمكن أيضاً حفظ مسارات تثبيت Ghidra الموجودة.
|
||||
|
||||
### قبل البدء
|
||||
يعرض التغييرات قبل تطبيقها وينشئ نسخة احتياطية من الإعدادات الموجودة. راجع [دليل التثبيت والإعداد](docs/installation.md) للمتطلبات والخيارات الإضافية.
|
||||
|
||||
- macOS 12 أو أحدث
|
||||
- Ubuntu 24.04+ أو Fedora 41+ أو Arch Linux 64 بت
|
||||
- Node.js 22.19+ أو 24.11+ وnpm المرفق مع Node
|
||||
### الاستخدام مع وكيل
|
||||
|
||||
يعرض `rea setup` خطة التغييرات كاملة ويطلب التأكيد قبل تطبيقها. لا يثبت أو يحدّث Homebrew أو Node.js أو npm. إذا كان [Hopper](https://www.hopperapp.com/) مفقودًا، يقترح Setup الحزمة الرسمية. Hopper برنامج منفصل يحتاج إلى ترخيص خاص به.
|
||||
بعد الإعداد، أعد تشغيل الوكيل واشرح التطبيق أو الميزة التي تريد استقصاءها. يدعم REA Claude Code وClaude Desktop وCodex وCursor وGemini CLI وWindsurf وDevin وOpenCode وAntigravity وGitHub Copilot CLI وCommand Code وVS Code. تكون تسجيلات REA الموجودة محددة افتراضياً، ويجب اختيار العملاء المكتشفين الآخرين يدوياً. يمكن للوكلاء الآخرين استخدام إعداد MCP أدناه.
|
||||
|
||||
إذا كان Ghidra 12.1.4 PUBLIC وJDK 21 الكامل بنواة 64 بت مثبتين مسبقًا على Linux 64 بت، فيمكن لـ Setup أيضًا تسجيل `GHIDRA_INSTALL_DIR` و`JAVA_HOME` الاختياري بعد الموافقة. لا ينزّل REA أو يثبت أو يعدّل Ghidra أو Java. يوفّر مزوّد Ghidra ضمن جلسة headless معزولة وللقراءة فقط الجرد، وفك الترجمة، والتجميع، وعلاقات الاستدعاء، والمراجع ذات الأنواع، وxref، وCFG، وملفات تحليل الدوال؛ وتبقى حالة الواجهة الرسومية وعمليات التعديل غير متاحة.
|
||||
يمكن تشغيل Hopper في الوضع التجريبي. إذا ظهرت رسالة عند التشغيل الأول، فاختر التجربة أو أدخل ترخيصًا موجودًا.
|
||||
|
||||
#### التثبيت على Linux واستكشاف الأخطاء
|
||||
### الاستخدام من الطرفية
|
||||
|
||||
على Ubuntu 24.04+ وFedora 41+ وArch Linux 64 بت، ينزّل REA حزمة DEB أو RPM أو Arch الرسمية، ويتحقق من الحجم وقيمة التحقق المنشورين، ثم يستخدم `apt-get` أو `dnf` أو `pacman` لحل الاعتماديات. عند التشغيل دون root يعرض `pkexec` طلب تفويض النظام. لا يستدعي REA الأمر `sudo`.
|
||||
|
||||
مسار المشغّل الافتراضي هو `/opt/hopper/bin/Hopper`. استخدم `HOPPER_LAUNCHER_PATH` للمسارات الأخرى. إذا أبلغ Doctor عن غياب محرك التحليل، شغّل `ldd /opt/hopper/bin/Hopper | grep 'not found'`، وثبّت المكتبات الناقصة ثم أعد `rea setup`. يشغّل REA إصدار Hopper التجريبي المدعوم على شاشة Xvfb خاصة ويختار وضع التجربة الذي يقدمه Hopper لكل جلسة تحليل؛ لا يلزم `DISPLAY` لسطح المكتب ولا ترخيص مدفوع، مع بقاء حدود الإصدار التجريبي التي يحددها المورّد. أضف `~/.local/bin` إلى `PATH` أيضًا.
|
||||
|
||||
### 1. إعداد REA
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
|
||||
npx --yes rea-agents@latest setup
|
||||
```
|
||||
|
||||
إذا طلب macOS أو برنامج التثبيت تأكيدًا، فأكمل الخطوة ثم شغّل الأمر نفسه مرة أخرى.
|
||||
|
||||
### 2. إعادة تشغيل وكيل البرمجة
|
||||
|
||||
يكتشف Setup كلاً من Claude Code وClaude Desktop وCodex وCursor وGemini CLI وWindsurf وDevin. ويهيئ تلقائيًا أول ستة عملاء عند اكتشافهم؛ أما Devin فيُبلّغ عن اكتشافه فقط ولا يُعدّله لعدم وجود حد موثق لإعداد MCP المحلي. أعد تشغيل العملاء الذين جرى إعدادهم كي يحمّلوا REA.
|
||||
|
||||
### 3. اطلب من الوكيل
|
||||
|
||||
يمكنك ذكر التطبيق باسمه. سيعثر وكيل البرمجة عليه ويمرر ملف البرنامج الذي يحتاجه REA.
|
||||
|
||||
```text
|
||||
استخدم REA لإجراء هندسة عكسية لتطبيق Notes. اشرح طريقة عمل البحث واعرض الأدلة،
|
||||
ثم ابنِ ميزة مشابهة لمشروعي باستخدام SQLite.
|
||||
```
|
||||
|
||||
إذا واجهت مشكلة، فشغّل:
|
||||
بعد الإعداد، شغّل:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest doctor
|
||||
rea uninstall
|
||||
rea uninstall --purge-data
|
||||
npx -y rea-agents@latest analyze /Applications/Notes.app
|
||||
```
|
||||
|
||||
### تثبيت أمر rea
|
||||
|
||||
ثبت واجهة سطر الأوامر:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
|
||||
```
|
||||
|
||||
يجب تثبيت Node.js وnpm مسبقًا. عند تشغيله من طرفية، يثبت البرنامج `rea` ويبدأ الإعداد.
|
||||
|
||||
يمكنك بدلًا من ذلك التثبيت باستخدام npm ثم تشغيل الإعداد:
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea setup
|
||||
```
|
||||
|
||||
### المتطلبات
|
||||
|
||||
- macOS 12 أو أحدث
|
||||
- Ubuntu 24.04+ أو Fedora 41+ أو Arch Linux بنواة 64 بت
|
||||
- Node.js 22.x (>=22.19) أو 24.x (>=24.11) أو 26+ وnpm
|
||||
|
||||
يتطلب تحليل الملفات التنفيذية الأصلية Hopper أو Ghidra. Hopper برنامج منفصل، وللوضع التجريبي قيود يحددها المورّد، لكن الترخيص المدفوع ليس إلزاميًا.
|
||||
|
||||
يدعم Ghidra نظامي Linux x64 وmacOS x64/arm64. ثبت Ghidra 12.1.4 وJDK 21 الكامل بنواة 64 بت بصورة منفصلة، ثم اضبط REA لاستخدامهما. يحتاج macOS أيضًا إلى أداة فك الترجمة الأصلية المطابقة لمعمارية الجهاز.
|
||||
|
||||
يستطيع Setup التحقق من التثبيت وحفظ المسارات؛ ولا يثبت أو يحدّث Ghidra أو Java أو Node.js أو npm أو Homebrew.
|
||||
|
||||
تحليل Ghidra على Windows غير متاح. لم تُنفذ بعد ضوابط ملكية العمليات وأذونات المجلدات الخاصة والتحقق الآمن من المسارات. حتى التثبيت الصحيح لـ Ghidra وJava لا يفعّل التحليل. راجع [Windows Ghidra P0](docs/windows-ghidra-p0.md).
|
||||
|
||||
### تشخيص المشكلات
|
||||
|
||||
يشخّص `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).
|
||||
|
||||
### التحديث والإزالة
|
||||
|
||||
- يحدّث `rea update` تثبيت REA الحالي.
|
||||
- يزيل `rea uninstall` تسجيلات الوكلاء وملفات سير العمل التي يديرها REA، ويترك Hopper مثبتًا.
|
||||
- يزيل `rea uninstall --purge-data` أيضًا ذاكرة REA المؤقتة وحالته. استخدمه فقط إذا أردت حذف هذه البيانات.
|
||||
|
||||
## حالة الدعم
|
||||
|
||||
يوفر REA عبر CLI وMCP تحليل الملفات التنفيذية الأصلية وتطبيقات JavaScript وElectron وتجميعات .NET والمواقع. تعتمد العمليات المتاحة على الجهاز والهدف وأداة التحليل المختارة.
|
||||
|
||||
- يدعم Hopper التحليل الأصلي والتعليقات؛ ويختلف سلوك واجهته الرسومية حسب المنصة.
|
||||
- يقدم Ghidra على Linux x64 وmacOS x64/arm64 عددًا قدره 22 عملية للقراءة فقط: الجرد والبحث وفك الترجمة والتعليمات والاستدعاءات والمراجع والأنواع. لا يقدم عمليات واجهة رسومية أو تعديل.
|
||||
- لسير عمل المتصفح وElectron وتشغيل العمليات متطلبات مستقلة للإعداد والموافقة وإدارة دورة الحياة. راجع [English README](README.md#current-status) للتفاصيل الكاملة.
|
||||
- تحليل Ghidra على Windows غير مفعّل. تابع التقدم في [#527](https://github.com/morluto/rea/issues/527).
|
||||
|
||||
## استقصاء كامل بطلب واحد
|
||||
|
||||
بعد الإعداد، يمكنك أن تطلب من وكيلك:
|
||||
@@ -159,22 +202,26 @@ rea uninstall --purge-data
|
||||
|
||||
## منصة أدوات التحقيق
|
||||
|
||||
| عائلة الأدوات | العدد | أمثلة |
|
||||
| --------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| فحص الملفات التنفيذية | 36 | الدوال، والشيفرة شبه المصدرية، والتعليمات، والسلاسل، والأسماء، والمراجع، والتعليقات |
|
||||
| التحليل المركب | 14 | النظرة العامة، و`inspect_native_api`، وفك الترجمة الدفعي، ومخططات الاستدعاء، والمراجع، واكتشاف الأنواع |
|
||||
| أدوات macOS الأصلية | 5 | بيانات Mach-O والتوقيعات وملفات plist والمعماريات وفك رموز Swift دون تشغيل Hopper |
|
||||
| رسم القطع الأثرية | 3 | فحص كامل مباشر للدلائل والحزم المدعومة، واستخراج معاملاتي محدد صراحة |
|
||||
| Managed PE/CLI | 8 | هوية PE/CLI، أعضاء البيانات الوصفية، بصمات CIL، تصريحات P/Invoke/الحدود الأصلية والتحقق منها، إسقاط رسم التطبيق، استيراد إعادة البناء من فك الترجمة، إعادة ربط الرموز، خطط ترابط وقت التشغيل ومقارنة الإصدارات |
|
||||
| مراقبة المتصفح | 9 | التقاط CDP محدود الأصل وتحليل الحزم وخرائط المصدر واكتشاف WebMCP والفروق والأدلة المرئية |
|
||||
| تحليل Electron | 5 | مراقبة سلبية ضمن جذور canonical، ورسم ثابت ومحدود للتطبيق، ومطابقة ثابتة/وقت تشغيل قائمة على Evidence |
|
||||
| وقت تشغيل JavaScript | 2 | اكتشاف أهداف Node/Electron Inspector المعتمدة للاتصال فقط، ومراقبة محدودة للبرامج النصية وسياقات التنفيذ دون تقييم أو تجهيز |
|
||||
| سير عمل التطبيقات | 10 | تتبع محدود ومقارنة إصدارات فريدة ومقارنة ثابتة لأشكال قيم إرجاع الصادرات وإعادة تشغيل وحدات معزولة ومعتمدة على Linux وتوصيف وقت التشغيل المُدار وإغلاق تغطية إعادة البناء ودفاتر التزامات حتمية |
|
||||
| جلسة الملف التنفيذي | 22 | فتح الأهداف وحزم الأدلة وتشغيل آلة إعادة محدودة ومقارنة العمليات والقطع والدوال وسجل المجهولات المتبقية |
|
||||
| عائلة الأدوات | العدد | الاستخدام |
|
||||
| --------------------- | ----: | -------------------------------------------------------------------------------------- |
|
||||
| فحص الملفات التنفيذية | 41 | الدوال والشيفرة شبه المصدرية والتعليمات والسلاسل والرموز والمراجع والتعليقات |
|
||||
| التحليل المركب | 14 | النظرة العامة وتحليل الدوال وفك الترجمة الدفعي ومخطط الاستدعاء وفحص Swift وObjC |
|
||||
| أدوات macOS الأصلية | 7 | بيانات Mach-O والتوقيعات وplist والمعماريات واستعادة أسماء Swift |
|
||||
| الملفات والحزم | 5 | فحص الدلائل والحزم وInterface Builder وموارد Apple والاستخراج |
|
||||
| .NET PE/CLI | 7 | هوية التجميع والبيانات الوصفية وCIL والاستدعاءات الأصلية والمقارنة واستيراد إعادة البناء |
|
||||
| مراقبة المتصفح | 9 | الصفحات والبرامج النصية وخرائط المصدر وWebMCP والصور ومقارنة الالتقاط |
|
||||
| تحليل Electron | 5 | مراقبة الصفحات وبنية التطبيق وربط النتائج الثابتة بنتائج التشغيل |
|
||||
| وقت تشغيل JavaScript | 2 | الاتصال بهدف Node/Electron Inspector موجود لمراقبة البرامج وسياقات التنفيذ |
|
||||
| سير عمل التطبيقات | 7 | تتبع الميزات ومقارنة الإصدارات وبنية قيم الإرجاع والتحقق من إعادة الإنشاء |
|
||||
| جلسة الملف التنفيذي | 21 | تبديل الأهداف وحفظ الأدلة ومقارنة العمليات والدوال وتسجيل الأسئلة غير المحسومة |
|
||||
|
||||
## خطة العمل
|
||||
|
||||
يشمل العمل التالي توسيع التحقق من الأهداف الأصلية، ومقارنة إصدارات JavaScript و.NET، ومراقبة وقت التشغيل والتحقق من إعادة الإنشاء. يتيح Setup بالفعل اختيار تكامل الوكيل وتثبيت Hopper. تثبيت أدوات تحليل أخرى ما زال عملًا لاحقًا. راجع [خطة التثبيت](docs/roadmap.md) و[تقييم أدوات التحليل](docs/provider-evaluation.md).
|
||||
|
||||
## استخدام REA مع وكلاء برمجة آخرين
|
||||
|
||||
يكتشف Setup كلاً من Claude Code وClaude Desktop وCodex وCursor وGemini CLI وWindsurf وDevin، ويهيئ تلقائيًا أول ستة عملاء. أما Devin فيُبلّغ عن اكتشافه فقط ولا يُعدّله. يمكن لأي وكيل برمجة يدعم خوادم MCP المحلية استخدام REA بالإعداد التالي:
|
||||
يدعم Setup Claude Code وClaude Desktop وCodex وCursor وGemini CLI وWindsurf وDevin وOpenCode وAntigravity وGitHub Copilot CLI وCommand Code وVS Code. تكون تسجيلات REA الموجودة محددة افتراضياً، ويجب اختيار العملاء المكتشفين الآخرين يدوياً. يمكن لأي وكيل يدعم خوادم MCP المحلية الاتصال باستخدام الإعداد التالي:
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
@@ -183,7 +230,7 @@ rea uninstall --purge-data
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@3.2.1", "mcp"]
|
||||
"args": ["-y", "rea-agents@4.0.1", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,7 +251,7 @@ flowchart LR
|
||||
Ghidra --> App
|
||||
```
|
||||
|
||||
يستخدم CLI وخادم MCP محرك التحليل نفسه. تغلق أوامر الطرفية التطبيق عند انتهائها، بينما تبقيه جلسة الوكيل مفتوحًا أثناء الاستقصاء.
|
||||
يستخدم CLI وخادم MCP سير عمل التحليل نفسه. تحرر أوامر الطرفية جلسة الجسر الخاصة بها عند انتهائها، بينما يمكن لجلسة الوكيل الحفاظ على الاتصال أثناء الاستقصاء. إغلاق جلسة REA لا يغلق تطبيق Hopper الذي يستخدمه المستخدم.
|
||||
|
||||
## أوامر CLI
|
||||
|
||||
@@ -221,15 +268,9 @@ npx -y rea-agents@latest compare /absolute/path/to/left-evidence.json /absolute/
|
||||
|
||||
## سلوك Hopper
|
||||
|
||||
يستطيع REA تشغيل Hopper تلقائيًا؛ لا حاجة إلى افتراض أنه يعمل مسبقًا. لكنه تطبيق macOS رسومي، لذلك قد يطلب التركيز أو يعرض حوارًا عندما يفرض Hopper قرارًا تفاعليًا. لا يمكن لـ REA ضمان تشغيل غير مرئي تمامًا لأن هذه الواجهة يملكها Hopper وmacOS.
|
||||
يشغّل REA برنامج Hopper عند الحاجة. على macOS قد تظهر نافذته في المقدمة أو يحتاج أحد الحوارات إلى استجابة منك. لا يمكن ضمان بقاء النافذة في الخلفية.
|
||||
|
||||
لتجنب مربع اختيار معمارية ملف شامل، حدد معطيات المحمل مسبقًا:
|
||||
|
||||
```bash
|
||||
export HOPPER_LOADER_ARGS_JSON='["-l", "Mach-O", "--aarch64"]'
|
||||
```
|
||||
|
||||
يدير REA دورة حياة Hopper ويحاول إغلاق العمليات التي بدأها عند انتهاء الأمر أو جلسة MCP.
|
||||
يحدد REA تنسيق الهدف ومعماريته لتجنب حوارات الاختيار المعتادة. عند إغلاق الجلسة، يوقف الجسر وينظف موارده الخاصة، لكنه لا يغلق تطبيق Hopper الذي تستخدمه.
|
||||
|
||||
## الأمان والخصوصية
|
||||
|
||||
@@ -258,7 +299,7 @@ export HOPPER_LOADER_ARGS_JSON='["-l", "Mach-O", "--aarch64"]'
|
||||
<details>
|
||||
<summary><strong>هل يعمل REA على Linux أو Windows؟</strong></summary>
|
||||
|
||||
نعم، يعمل REA على macOS 12+ وUbuntu 24.04+ وFedora 41+ وArch Linux 64 بت؛ Windows غير مدعوم حاليًا. يستخدم التحليل العميق Hopper أو مزوّد Ghidra للقراءة فقط الذي يختاره المستدعي؛ ويتطلب Ghidra على Linux الإصدار 12.1.4 وJDK 21 الكامل.
|
||||
يدعم REA نظام macOS 12+ وUbuntu 24.04+ وFedora 41+ وArch Linux بنواة 64 بت. يعمل تحليل Ghidra للقراءة فقط على Linux x64 وmacOS x64/arm64 مع Ghidra 12.1.4 وJDK 21 الكامل. تحليل Ghidra على Windows غير متاح بسبب الضوابط الأصلية غير المنفذة؛ راجع [Windows Ghidra P0](docs/windows-ghidra-p0.md).
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
+121
-75
@@ -4,7 +4,7 @@
|
||||
|
||||
# REA:あらゆるものをリバースエンジニアリング
|
||||
|
||||
### コーディングエージェントがあらゆるものをリバースエンジニアリングするための、ひとつの CLI / MCP サーバー
|
||||
### エージェントでアプリの動作からネイティブバイナリまで調べる。
|
||||
|
||||
**気になる機能を見つけ、仕組みを理解し、自分の形で実装する。**
|
||||
|
||||
@@ -13,35 +13,51 @@
|
||||
[](#調査ツールカタログ)
|
||||
[](https://nodejs.org/)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
[クイックスタート](#クイックスタート) · [バイナリから動作へ](#バイナリから動作へ) · [ツールカタログ](#調査ツールカタログ) · [仕組み](#仕組み) · [FAQ](#faq)
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/82054/weekly?language=TypeScript" alt="morluto/rea | Trendshift" width="250" height="55" /></a>
|
||||
|
||||
[クイックスタート](#クイックスタート) · [現在の対応状況](#現在の対応状況) · [バイナリから動作へ](#バイナリから動作へ) · [調査ツールカタログ](#調査ツールカタログ) · [ロードマップ](#ロードマップ) · [仕組み](#仕組み)
|
||||
|
||||
<table aria-label="REA community">
|
||||
<tr>
|
||||
<td align="center" width="360">
|
||||
<a href="https://discord.gg/GkcryMnJDM">
|
||||
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
|
||||
<strong>リバースエンジニアリングのコミュニティに参加</strong>
|
||||
</a><br />
|
||||
<sub>Discord · 質問と回答 · 成果の共有</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
<code>curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash</code>
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="Hopper 内で解析ブリッジを起動し、ネイティブバイナリを調べる REA" width="1200" />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
アプリの気になる機能を自分のプロダクトにも取り入れたいと思ったことはありませんか。ソースコードがなくても、そのアプリをコーディングエージェントに渡せます。REA を使えば、エージェントが機能を調査して仕組みを理解し、あなたの技術スタック、デザイン、要件に合わせた形で実装できます。
|
||||
アプリの機能を自分のプロダクトにも取り入れたいときは、REA を使ってエージェントに調査を依頼できます。ソースコードがなくても、アプリを調べ、仕組みと根拠を示し、あなたのプロジェクト向けに同様の機能を実装できます。
|
||||
|
||||
REA はこの流れをひとつの CLI / MCP サーバーで実現します。エージェントはコンパイル済みのアプリを調べ、機能の流れを追い、分かったことを通常のコーディング作業に活用できます。複雑なリバースエンジニアリングツールは REA がひとつのインターフェースの裏側で扱います。
|
||||
REA はネイティブバイナリ、JavaScript/Electron アプリ、.NET アセンブリ、Web サイトの解析ツールを提供します。エージェントからもターミナルからも使えます。解析はローカルで実行され、結果には根拠と制限が含まれます。
|
||||
|
||||
Setup はエージェントを設定し、既存の Hopper または Ghidra に接続します。承認後に Hopper をインストールすることもできます。
|
||||
|
||||
## エージェントに頼むだけ
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
次のように依頼します:
|
||||
[セットアップ](#クイックスタート)後にエージェントを再起動し、次のように依頼します:
|
||||
|
||||
```text
|
||||
メモアプリをリバースエンジニアリングしてください。
|
||||
検索の仕組みと判断の理由を示し、私のプロジェクト向けに同様の機能を実装してください。
|
||||
メモアプリの検索機能を調べて根拠を示し、私のプロジェクト向けに同様の機能を実装してください。
|
||||
```
|
||||
|
||||
メモアプリは例にすぎません。調べたいアプリを指定するか、まず概要から始めるよう依頼できます。
|
||||
メモアプリを調べたいアプリに置き換えるか、まず概要を依頼してください。
|
||||
|
||||
## バイナリから動作へ
|
||||
|
||||
@@ -53,70 +69,96 @@ REA は調査をバイナリ上の根拠に結び付けます。元のソース
|
||||
|
||||
## REA を選ぶ理由
|
||||
|
||||
| | |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------- |
|
||||
| **エージェント向け** | コンパイル済みアプリについて質問し、推測ではなく根拠を集めさせることができます。 |
|
||||
| **CLI と MCP** | ターミナルとコーディングエージェントから同じリバースエンジニアリング機能を使えます。 |
|
||||
| **複雑さを処理** | ツール設定、アプリの読み込み、調査の維持、終了後のクリーンアップを REA が担います。 |
|
||||
| **一連の調査に対応** | 最初の概要から疑似コード、呼び出し関係、型、実装の手掛かりまで掘り下げられます。 |
|
||||
| **ローカルで解析** | 解析は Mac 上で実行され、REA がバイナリをホスト型解析サービスへ送ることはありません。 |
|
||||
| **コンテキストを維持** | 質問ごとに解析を最初からやり直さず、複数のバイナリを続けて調査できます。 |
|
||||
| | |
|
||||
| ---------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| **エージェント向け** | コンパイル済みアプリについて質問し、推測ではなく根拠を集めさせることができます。 |
|
||||
| **CLI と MCP** | ターミナルとコーディングエージェントから同じリバースエンジニアリング機能を使えます。 |
|
||||
| **複雑さを処理** | ツール設定、アプリの読み込み、調査の維持、終了後のクリーンアップを REA が担います。 |
|
||||
| **一連の調査に対応** | 最初の概要から疑似コード、呼び出し関係、型、実装の手掛かりまで掘り下げられます。 |
|
||||
| **ローカルで解析** | 解析は対応するローカルホストで実行され、REA がバイナリをホスト型解析サービスへ送ることはありません。 |
|
||||
| **コンテキストを維持** | 質問ごとに解析を最初からやり直さず、複数のバイナリを続けて調査できます。 |
|
||||
|
||||
## クイックスタート
|
||||
|
||||
### コーディングエージェントから(推奨)
|
||||
### セットアップを実行(推奨)
|
||||
|
||||
REA をエージェントで使うための設定を行います:
|
||||
|
||||
```bash
|
||||
npx --yes rea-agents@latest setup
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
エージェントに REA のセットアップを依頼してください。Mac を確認し、必要なインストールを説明して承認を求め、システムプロンプトを案内します。完全なツールを読み込むため再起動を求められた場合は、セットアップ後に再起動してください。
|
||||
Setup は最初に連携するエージェントを複数選択できるようにします。既存の REA 登録は初期選択されますが、検出されただけのクライアントは自動選択されず、未設定のクライアントも選べます。具体的なパスと変更内容を確認してから承認してください。選択したエージェントには通常 REA のワークフローをインストールします。Hopper は別の任意操作で、個別の承認が必要です。既存の Ghidra のパスも登録できます。
|
||||
|
||||
### 始める前に
|
||||
変更は事前に表示され、既存の設定はバックアップされます。要件と追加オプションは[インストールガイド](docs/installation.md)を参照してください。
|
||||
|
||||
- macOS 12 以降
|
||||
- Ubuntu 24.04+、Fedora 41+、または 64 ビット Arch Linux
|
||||
- Node.js 22.19+ または 24.11+ と、Node に付属する npm
|
||||
### エージェントで使う
|
||||
|
||||
`rea setup` は変更計画を表示し、確認後に適用します。Homebrew、Node.js、npm をインストールまたは更新しません。[Hopper](https://www.hopperapp.com/) がない場合は公式パッケージを提案します。Hopper は別製品で、ライセンスは別途必要です。
|
||||
設定後にエージェントを再起動し、調べたいアプリや機能を説明します。REA は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code、VS Code に対応します。既存の REA 登録は初期選択され、それ以外の検出済みクライアントは選択が必要です。その他のエージェントは下記の MCP 設定を使えます。
|
||||
|
||||
64 ビット Linux に Ghidra 12.1.4 PUBLIC と完全な 64 ビット JDK 21 がすでにある場合、Setup は承認後に `GHIDRA_INSTALL_DIR` と任意の `JAVA_HOME` も登録できます。REA は Ghidra や Java をダウンロード、インストール、変更しません。Ghidra プロバイダーは隔離された読み取り専用 headless セッションで、インベントリ、逆コンパイル、アセンブリ、呼び出し関係、型付き参照、xref、CFG、関数 dossier を提供します。GUI 状態と変更操作は引き続き利用できません。
|
||||
Hopper はデモモードで使えます。初回起動の画面ではデモを選択するか、既存のライセンスを入力してください。
|
||||
|
||||
#### Linux のインストールとトラブルシューティング
|
||||
### ターミナルで使う
|
||||
|
||||
Ubuntu 24.04+、Fedora 41+、64 ビット Arch Linux では、REA が公式の DEB、RPM、または Arch パッケージを取得し、公開されたサイズとチェックサムを検証してから `apt-get`、`dnf`、または `pacman` で依存関係を解決します。root 以外では `pkexec` がシステム承認を表示します。REA は `sudo` を呼び出しません。
|
||||
|
||||
既定のランチャーは `/opt/hopper/bin/Hopper` です。別の場所では `HOPPER_LAUNCHER_PATH` を設定してください。Doctor が解析エンジン不足を報告した場合は `ldd /opt/hopper/bin/Hopper | grep 'not found'` を実行し、不足ライブラリを入れて `rea setup` を再実行します。REA は対応する Hopper デモ版を専用 Xvfb ディスプレイで起動し、解析セッションごとに Hopper が提示するデモモードを選択します。デスクトップの `DISPLAY` や有料ライセンスは不要ですが、デモ版にはベンダー所定の制限があります。`~/.local/bin` も `PATH` に追加してください。
|
||||
|
||||
```bash
|
||||
# 1. REA をセットアップ
|
||||
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
|
||||
npx --yes rea-agents@latest setup
|
||||
```
|
||||
|
||||
macOS やインストーラーから確認を求められた場合は、その操作を完了してから同じコマンドをもう一度実行してください。
|
||||
|
||||
### 2. コーディングエージェントを再起動
|
||||
|
||||
Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin を検出します。検出した最初の 6 クライアントは自動設定しますが、Devin はローカル MCP 設定境界が文書化されていないため、検出結果を報告するだけで変更しません。設定されたクライアントを再起動して REA を読み込んでください。
|
||||
|
||||
### 3. エージェントに依頼
|
||||
|
||||
アプリは名前で指定できます。コーディングエージェントがアプリを探し、REA に必要なプログラムファイルを渡します。
|
||||
|
||||
```text
|
||||
REA でメモアプリをリバースエンジニアリングしてください。検索機能の仕組みと
|
||||
根拠を示し、SQLite を使って私のプロジェクト向けに同様の機能を実装してください。
|
||||
```
|
||||
|
||||
問題がある場合は次を実行します:
|
||||
設定後に実行します:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest doctor
|
||||
rea uninstall
|
||||
rea uninstall --purge-data
|
||||
npx -y rea-agents@latest analyze /Applications/Notes.app
|
||||
```
|
||||
|
||||
### rea コマンドをインストール
|
||||
|
||||
コマンドラインツールをインストールします:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
|
||||
```
|
||||
|
||||
Node.js と npm を先にインストールしてください。ターミナルで実行すると、インストーラーは `rea` を追加してセットアップを開始します。
|
||||
|
||||
npm でインストールし、セットアップを実行することもできます:
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea setup
|
||||
```
|
||||
|
||||
### 要件
|
||||
|
||||
- macOS 12 以降
|
||||
- Ubuntu 24.04+、Fedora 41+、または 64 ビット Arch Linux
|
||||
- Node.js 22.x (>=22.19)、24.x (>=24.11)、または 26+ と npm
|
||||
|
||||
ネイティブバイナリ解析には Hopper または Ghidra が必要です。Hopper は別製品です。デモにはベンダー所定の制限がありますが、有料ライセンスは必須ではありません。
|
||||
|
||||
Ghidra は Linux x64 と macOS x64/arm64 に対応します。Ghidra 12.1.4 と完全な 64 ビット JDK 21 を別途インストールし、REA で使うように設定してください。macOS ではホストのアーキテクチャに合うネイティブデコンパイラーも必要です。
|
||||
|
||||
Setup はインストールを確認し、パスを保存できます。Ghidra、Java、Node.js、npm、Homebrew のインストールや更新は行いません。
|
||||
|
||||
Windows の Ghidra 解析は利用できません。プロセスの所有権、プライベートディレクトリの権限、安全なパス検証が未実装のため、Ghidra と Java を正しくインストールしても有効になりません。[Windows Ghidra P0](docs/windows-ghidra-p0.md)を参照してください。
|
||||
|
||||
### トラブルシューティング
|
||||
|
||||
`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)を参照してください。
|
||||
|
||||
### 更新とアンインストール
|
||||
|
||||
- `rea update` は現在の REA インストールを更新します。
|
||||
- `rea uninstall` は REA が管理するエージェント登録とワークフローファイルを削除します。Hopper は残ります。
|
||||
- `rea uninstall --purge-data` は REA のキャッシュと状態も削除します。それらを削除したい場合にだけ使ってください。
|
||||
|
||||
## 現在の対応状況
|
||||
|
||||
REA は CLI と MCP からネイティブバイナリ、JavaScript/Electron アプリ、.NET アセンブリ、Web サイトを解析できます。使える操作はホスト、対象、選択した解析ツールによって異なります。
|
||||
|
||||
- Hopper はネイティブ解析と注釈操作に対応します。GUI の動作はプラットフォームによって異なります。
|
||||
- Ghidra は Linux x64 と macOS x64/arm64 で、一覧、検索、逆コンパイル、アセンブリ、呼び出し、参照、命令、型の検査など 22 の読み取り専用操作を提供します。GUI 操作や変更操作はありません。
|
||||
- ブラウザー、Electron、プロセスの実行時ワークフローには、それぞれ設定、承認、終了処理の要件があります。詳細は [English README](README.md#current-status) を参照してください。
|
||||
- Windows の Ghidra 解析は未対応です。進捗は [#527](https://github.com/morluto/rea/issues/527) で確認できます。
|
||||
|
||||
## ひとつのプロンプトで調査を完結
|
||||
|
||||
```text
|
||||
@@ -149,22 +191,26 @@ REA は手順 1〜5 のバイナリ解析を処理し、手順 6 はエージェ
|
||||
|
||||
## 調査ツールカタログ
|
||||
|
||||
| ツール群 | 数 | 例 |
|
||||
| ---------------------- | --: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| バイナリ調査 | 36 | プロシージャ、疑似コード、アセンブリ、文字列、名前、セグメント、callers、callees、xrefs、注釈 |
|
||||
| 合成解析 | 14 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, Swift / ObjC 検出 |
|
||||
| macOS ネイティブ | 5 | Mach-O メタデータ、署名、plist、アーキテクチャ、Swift デマングル。Hopper 起動不要 |
|
||||
| アーティファクトグラフ | 3 | ディレクトリと対応パッケージの完全なインライン調査、および明示選択されたトランザクション抽出 |
|
||||
| Managed PE/CLI | 8 | PE/CLI 識別、メタデータメンバー、CIL ハッシュ、P/Invoke/ネイティブ境界宣言と検証、アプリケーショングラフ投影、逆コンパイル再構成インポート、構造的 token 再マッピング、ランタイム相関計画、バージョン比較 |
|
||||
| ブラウザ観察 | 9 | origin 限定 CDP 取得、bundle/source map 解析、WebMCP 検出、セッション、capture diff、視覚証拠 |
|
||||
| Electron 解析 | 5 | canonical ファイルルート内の受動観察、有界な静的アプリマッピング、Evidence に基づく静的/実行時の照合 |
|
||||
| JavaScript ランタイム | 2 | 承認済みのアタッチ専用 Node/Electron Inspector ターゲット検出と、評価や計装を行わない有界なスクリプト/実行コンテキスト観察 |
|
||||
| アプリケーション | 10 | 有界なクロスレイヤー追跡、一意限定のバージョン比較、静的なエクスポート戻り値形状比較、承認済み Linux 隔離 extracted-module replay、マネージドランタイム特性評価、再構築カバレッジ閉包、決定論的な義務台帳 |
|
||||
| バイナリセッション | 22 | `open_binary`、`binary_session`、証拠バンドル、有限リプレイ状態機械、プロセス/成果物/関数比較、残余未知レジストリ |
|
||||
| ツール分類 | 数 | 用途 |
|
||||
| -------------------- | --: | --------------------------------------------------------------------------- |
|
||||
| バイナリ検査 | 41 | 関数、疑似コード、アセンブリ、文字列、シンボル、参照、注釈 |
|
||||
| 組み合わせた解析 | 14 | 概要、関数解析、一括逆コンパイル、呼び出しグラフ、Swift と ObjC |
|
||||
| macOS ネイティブ | 7 | Mach-O メタデータ、署名、plist、アーキテクチャ、Swift 名の復元 |
|
||||
| ファイルとパッケージ | 5 | ディレクトリとパッケージ、Interface Builder、Apple リソース、抽出 |
|
||||
| .NET PE/CLI | 7 | アセンブリ識別、メタデータ、CIL、ネイティブ呼び出し、比較、再構築の取り込み |
|
||||
| ブラウザー観察 | 9 | ページ、スクリプト、ソースマップ、WebMCP、スクリーンショット、比較 |
|
||||
| Electron 解析 | 5 | ページ観察、アプリ構造、静的・実行時結果の対応付け |
|
||||
| JavaScript 実行時 | 2 | 既存の Node/Electron Inspector に接続し、スクリプトと実行コンテキストを観察 |
|
||||
| アプリのワークフロー | 7 | 機能追跡、バージョン比較、戻り値構造の比較、再実装の検証 |
|
||||
| バイナリセッション | 21 | 対象切り替え、根拠の保存、プロセス・関数比較、未解決事項の記録 |
|
||||
|
||||
## ロードマップ
|
||||
|
||||
次の作業はネイティブ対象の検証拡大、JavaScript と .NET のバージョン比較、実行時の観察と再実装の検証です。Setup はエージェント連携と Hopper のインストールを選択できます。他の解析ツールのインストールは今後の作業です。[インストールのロードマップ](docs/roadmap.md)と[解析ツールの評価](docs/provider-evaluation.md)を参照してください。
|
||||
|
||||
## 他のコーディングエージェントで使う
|
||||
|
||||
Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin を検出し、最初の 6 クライアントを自動設定します。Devin は検出結果を報告するだけで変更しません。ローカル MCP サーバーに対応するコーディングエージェントなら、次の設定で REA を利用できます。
|
||||
Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code、VS Code に対応します。既存の REA 登録は初期選択され、それ以外の検出済みクライアントは選択が必要です。ローカル MCP サーバーに対応するエージェントは、次の設定でも接続できます。
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
@@ -173,7 +219,7 @@ Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@3.2.1", "mcp"]
|
||||
"args": ["-y", "rea-agents@4.0.1", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,7 +239,7 @@ flowchart LR
|
||||
Ghidra --> App
|
||||
```
|
||||
|
||||
CLI と MCP サーバーは同じ解析エンジンを使います。ターミナルのコマンドは終了時にアプリを閉じ、エージェントのセッションは調査中アプリを開いたままにします。
|
||||
CLI と MCP サーバーは同じ解析処理を使います。ターミナルコマンドは完了後に自分のブリッジセッションを解放し、エージェントセッションは調査中の接続を維持できます。REA セッションを閉じても、ユーザーが利用中の Hopper は終了しません。
|
||||
|
||||
## CLI
|
||||
|
||||
@@ -229,7 +275,7 @@ REA は必要なときに Hopper を起動します。Hopper のランチャー
|
||||
|
||||
<details><summary><strong>Hopper を先に起動する必要がありますか?</strong></summary>
|
||||
|
||||
いいえ。REA が必要時に起動します。すでに起動している Hopper にも対応します。
|
||||
いいえ。REA が必要時に起動します。Hopper が起動済みでも使えますが、既存の GUI ドキュメントには接続せず、新しい解析ドキュメントを開きます。
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
+122
-75
@@ -4,7 +4,7 @@
|
||||
|
||||
# REA: 무엇이든 리버스 엔지니어링
|
||||
|
||||
### 코딩 에이전트가 무엇이든 리버스 엔지니어링할 수 있게 하는 하나의 CLI 및 MCP 서버
|
||||
### 에이전트로 앱의 동작부터 네이티브 바이너리까지 조사하세요.
|
||||
|
||||
**마음에 드는 기능을 찾고, 작동 방식을 이해하고, 원하는 방식으로 구현하세요.**
|
||||
|
||||
@@ -13,35 +13,52 @@
|
||||
[](#조사-도구-카탈로그)
|
||||
[](https://nodejs.org/)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
[빠른 시작](#빠른-시작) · [바이너리에서 동작까지](#바이너리에서-동작까지) · [도구 카탈로그](#조사-도구-카탈로그) · [작동 방식](#작동-방식) · [FAQ](#faq)
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/82054/weekly?language=TypeScript" alt="morluto/rea | Trendshift" width="250" height="55" /></a>
|
||||
|
||||
[빠른 시작](#빠른-시작) · [현재 지원 범위](#현재-지원-범위) · [바이너리에서 동작까지](#바이너리에서-동작까지) · [조사 도구 카탈로그](#조사-도구-카탈로그) · [로드맵](#로드맵) · [작동 방식](#작동-방식)
|
||||
|
||||
<table aria-label="REA community">
|
||||
<tr>
|
||||
<td align="center" width="360">
|
||||
<a href="https://discord.gg/GkcryMnJDM">
|
||||
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
|
||||
<strong>리버스 엔지니어링 커뮤니티에 참여하세요</strong>
|
||||
</a><br />
|
||||
<sub>Discord · 질문과 답변 · 작업 공유</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
<code>curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash</code>
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="Hopper에서 분석 브리지를 시작하고 네이티브 바이너리를 조사하는 REA" width="1200" />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
어떤 앱에서 마음에 드는 기능을 자신의 제품에도 넣고 싶나요? 소스 코드가 없어도 앱을 코딩 에이전트에 전달할 수 있습니다. REA를 사용하면 에이전트가 기능을 조사하고 작동 방식을 이해한 뒤, 사용자의 기술 스택, 디자인, 요구 사항에 맞는 버전을 구현할 수 있습니다.
|
||||
앱에서 마음에 드는 기능을 찾았다면 에이전트에게 REA로 조사해 달라고 요청하세요. 소스 코드가 없어도 앱을 분석하고, 기능의 동작과 근거를 설명한 뒤, 프로젝트에 맞는 기능을 구현할 수 있습니다.
|
||||
|
||||
REA는 이 과정을 하나의 CLI 및 MCP 서버로 제공합니다. 에이전트는 컴파일된 앱을 조사하고 기능의 작동 방식을 추적한 뒤 알아낸 내용을 일반 코딩 작업에 활용할 수 있습니다. 복잡한 리버스 엔지니어링 도구는 REA가 하나의 인터페이스 뒤에서 처리합니다.
|
||||
REA는 네이티브 바이너리, JavaScript/Electron 앱, .NET 어셈블리, 웹사이트 분석 도구를 제공합니다. 에이전트와 터미널에서 같은 도구를 사용할 수 있습니다. 분석은 로컬에서 실행되며 결과에는 근거와 제한 사항이 포함됩니다.
|
||||
|
||||
Setup은 에이전트를 설정하고 기존 Hopper 또는 Ghidra 설치에 연결합니다. 승인 후 Hopper를 설치할 수도 있습니다.
|
||||
|
||||
## 에이전트에게 바로 요청하기
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
그런 다음 요청하세요.
|
||||
[설정](#빠른-시작)을 마친 뒤 에이전트를 다시 시작하고 요청하세요.
|
||||
|
||||
```text
|
||||
REA를 설정하고 메모 앱을 리버스 엔지니어링해 주세요. 검색 기능의 작동 방식과
|
||||
그렇게 판단한 이유를 보여 주고, 제 프로젝트에 비슷한 기능을 구현해 주세요.
|
||||
메모 앱의 검색 기능이 어떻게 동작하는지 조사하고 근거를 보여 주세요.
|
||||
그다음 제 프로젝트에 비슷한 기능을 구현해 주세요.
|
||||
```
|
||||
|
||||
메모 앱은 예시일 뿐입니다. 이해하고 싶은 앱을 지정하거나 먼저 개요부터 살펴보라고 요청할 수 있습니다.
|
||||
메모 앱 대신 조사할 앱을 지정하거나 먼저 개요를 요청할 수 있습니다.
|
||||
|
||||
## 바이너리에서 동작까지
|
||||
|
||||
@@ -53,70 +70,96 @@ REA는 분석을 바이너리 증거에 근거하게 합니다. 원본 소스
|
||||
|
||||
## REA를 사용하는 이유
|
||||
|
||||
| | |
|
||||
| ------------------ | ---------------------------------------------------------------------------------- |
|
||||
| **에이전트용** | 컴파일된 앱에 관해 질문하고 추측 대신 증거를 수집하게 합니다. |
|
||||
| **CLI와 MCP** | 터미널과 코딩 에이전트에서 동일한 리버스 엔지니어링 기능을 사용합니다. |
|
||||
| **복잡성 처리** | 도구 설정, 앱 열기, 조사 유지, 작업 후 정리를 REA가 처리합니다. |
|
||||
| **전체 조사 과정** | 첫 개요에서 의사 코드, 호출 관계, 타입, 구현 단서까지 이어서 조사합니다. |
|
||||
| **로컬 분석** | 분석은 Mac에서 실행되며 REA는 바이너리를 호스팅 분석 서비스에 업로드하지 않습니다. |
|
||||
| **컨텍스트 유지** | 질문마다 분석을 처음부터 시작하지 않고 여러 바이너리를 연속으로 조사합니다. |
|
||||
| | |
|
||||
| ------------------ | --------------------------------------------------------------------------------------------------- |
|
||||
| **에이전트용** | 컴파일된 앱에 관해 질문하고 추측 대신 증거를 수집하게 합니다. |
|
||||
| **CLI와 MCP** | 터미널과 코딩 에이전트에서 동일한 리버스 엔지니어링 기능을 사용합니다. |
|
||||
| **복잡성 처리** | 도구 설정, 앱 열기, 조사 유지, 작업 후 정리를 REA가 처리합니다. |
|
||||
| **전체 조사 과정** | 첫 개요에서 의사 코드, 호출 관계, 타입, 구현 단서까지 이어서 조사합니다. |
|
||||
| **로컬 분석** | 분석은 지원되는 로컬 호스트에서 실행되며 REA는 바이너리를 호스팅 분석 서비스에 업로드하지 않습니다. |
|
||||
| **컨텍스트 유지** | 질문마다 분석을 처음부터 시작하지 않고 여러 바이너리를 연속으로 조사합니다. |
|
||||
|
||||
## 빠른 시작
|
||||
|
||||
### 코딩 에이전트에서 시작하기(권장)
|
||||
### 설정 실행하기(권장)
|
||||
|
||||
REA를 에이전트에 연결하세요.
|
||||
|
||||
```bash
|
||||
npx --yes rea-agents@latest setup
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
에이전트에게 REA 설정을 요청하세요. Mac을 확인하고 필요한 설치 내용을 설명한 뒤 승인을 요청하며 시스템 프롬프트를 안내합니다. 전체 도구를 불러오기 위해 재시작하라고 하면 설정 후 에이전트를 재시작하세요.
|
||||
Setup은 먼저 연결할 에이전트를 여러 개 선택하도록 안내합니다. 기존 REA 등록은 기본 선택되며, 감지만 된 클라이언트는 자동 선택되지 않습니다. 아직 설정되지 않은 클라이언트도 직접 선택할 수 있습니다. 정확한 경로와 변경 사항을 검토한 뒤 승인하세요. 선택한 에이전트에는 기본적으로 REA 워크플로가 설치됩니다. Hopper는 별도의 선택 사항이며 따로 승인해야 합니다. 기존 Ghidra 경로도 등록할 수 있습니다.
|
||||
|
||||
### 시작하기 전에
|
||||
변경 전에 계획을 표시하고 기존 설정을 백업합니다. 요구 사항과 추가 옵션은 [설치 안내](docs/installation.md)를 참고하세요.
|
||||
|
||||
- macOS 12 이상
|
||||
- Ubuntu 24.04+, Fedora 41+ 또는 64비트 Arch Linux
|
||||
- Node.js 22.19+ 또는 24.11+ 및 Node와 함께 제공되는 npm
|
||||
### 에이전트에서 사용하기
|
||||
|
||||
`rea setup`은 전체 변경 계획을 표시하고 확인 후 적용합니다. Homebrew, Node.js 또는 npm을 설치하거나 업데이트하지 않습니다. [Hopper](https://www.hopperapp.com/)가 없으면 공식 패키지 설치를 제안합니다. Hopper는 별도 라이선스가 필요한 독립 소프트웨어입니다.
|
||||
설정 후 에이전트를 다시 시작하고 조사할 앱이나 기능을 설명하세요. REA는 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity, GitHub Copilot CLI, Command Code, VS Code를 지원합니다. 기존 REA 등록은 기본 선택되고, 그 밖의 감지된 클라이언트는 직접 선택해야 합니다. 다른 에이전트는 아래 MCP 설정을 사용할 수 있습니다.
|
||||
|
||||
64비트 Linux에 Ghidra 12.1.4 PUBLIC과 완전한 64비트 JDK 21이 이미 설치되어 있다면 Setup은 승인 후 `GHIDRA_INSTALL_DIR`과 선택 사항인 `JAVA_HOME`도 등록할 수 있습니다. REA는 Ghidra나 Java를 다운로드, 설치 또는 수정하지 않습니다. Ghidra 공급자는 격리된 읽기 전용 headless 세션에서 인벤토리, 디컴파일, 어셈블리, 호출 관계, 형식이 지정된 참조, xref, CFG 및 함수 dossier를 제공합니다. GUI 상태와 변경 작업은 계속 사용할 수 없습니다.
|
||||
Hopper는 데모 모드로 사용할 수 있습니다. 첫 실행 안내가 나오면 데모를 선택하거나 기존 라이선스를 입력하세요.
|
||||
|
||||
#### Linux 설치 및 문제 해결
|
||||
### 터미널에서 실행하기
|
||||
|
||||
Ubuntu 24.04+, Fedora 41+, 64비트 Arch Linux에서 REA는 공식 DEB, RPM 또는 Arch 패키지를 내려받아 게시된 크기와 체크섬을 검증한 뒤 `apt-get`, `dnf` 또는 `pacman`으로 의존성을 설치합니다. root가 아니면 `pkexec`가 시스템 승인 창을 표시합니다. REA는 `sudo`를 호출하지 않습니다.
|
||||
|
||||
기본 실행 파일은 `/opt/hopper/bin/Hopper`입니다. 다른 위치에 설치했다면 `HOPPER_LAUNCHER_PATH`를 설정하세요. Doctor가 분석 엔진 누락을 보고하면 `ldd /opt/hopper/bin/Hopper | grep 'not found'`를 실행하고 누락된 라이브러리를 설치한 다음 `rea setup`을 다시 실행하세요. REA는 지원되는 Hopper 데모 빌드를 전용 Xvfb 디스플레이에서 실행하고 분석 세션마다 Hopper가 제공하는 데모 모드를 선택합니다. 데스크톱 `DISPLAY`나 유료 라이선스는 필요하지 않지만 데모에는 공급업체가 정한 제한이 있습니다. `~/.local/bin`도 `PATH`에 추가해야 합니다.
|
||||
|
||||
```bash
|
||||
# 1. REA 설정
|
||||
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
|
||||
npx --yes rea-agents@latest setup
|
||||
```
|
||||
|
||||
macOS나 설치 프로그램이 확인을 요청하면 해당 절차를 완료하고 같은 명령을 다시 실행하세요.
|
||||
|
||||
### 2. 코딩 에이전트 재시작
|
||||
|
||||
Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin을 감지합니다. 감지된 앞의 여섯 클라이언트는 자동으로 구성하지만, Devin은 문서화된 로컬 MCP 구성 경계가 없어 감지만 보고하고 변경하지 않습니다. 구성된 클라이언트를 다시 시작해 REA를 불러오세요.
|
||||
|
||||
### 3. 에이전트에게 요청
|
||||
|
||||
앱 이름만 말해도 됩니다. 코딩 에이전트가 앱을 찾아 REA에 필요한 프로그램 파일을 전달합니다.
|
||||
|
||||
```text
|
||||
REA로 메모 앱을 리버스 엔지니어링해 주세요. 검색 기능의 작동 방식과 증거를 보여 주고,
|
||||
SQLite를 사용해 제 프로젝트에 비슷한 기능을 구현해 주세요.
|
||||
```
|
||||
|
||||
문제가 있으면 다음을 실행하세요.
|
||||
설정 후 실행하세요.
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest doctor
|
||||
rea uninstall
|
||||
rea uninstall --purge-data
|
||||
npx -y rea-agents@latest analyze /Applications/Notes.app
|
||||
```
|
||||
|
||||
### rea 명령 설치하기
|
||||
|
||||
명령줄 도구를 설치하세요.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
|
||||
```
|
||||
|
||||
Node.js와 npm이 먼저 설치되어 있어야 합니다. 터미널에서 실행하면 설치 프로그램이 `rea`를 설치하고 설정을 시작합니다.
|
||||
|
||||
npm으로 설치한 뒤 설정을 실행할 수도 있습니다.
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea setup
|
||||
```
|
||||
|
||||
### 요구 사항
|
||||
|
||||
- macOS 12 이상
|
||||
- Ubuntu 24.04+, Fedora 41+ 또는 64비트 Arch Linux
|
||||
- Node.js 22.x (>=22.19), 24.x (>=24.11) 또는 26+와 npm
|
||||
|
||||
네이티브 바이너리 분석에는 Hopper 또는 Ghidra가 필요합니다. Hopper는 별도 소프트웨어입니다. 데모에는 공급업체의 제한이 있지만 유료 라이선스가 필수는 아닙니다.
|
||||
|
||||
Ghidra는 Linux x64와 macOS x64/arm64를 지원합니다. Ghidra 12.1.4와 완전한 64비트 JDK 21을 별도로 설치한 뒤 REA가 사용하도록 설정하세요. macOS에서는 호스트 아키텍처에 맞는 네이티브 디컴파일러도 필요합니다.
|
||||
|
||||
Setup은 설치를 확인하고 경로를 저장할 수 있습니다. Ghidra, Java, Node.js, npm, Homebrew를 설치하거나 업데이트하지 않습니다.
|
||||
|
||||
Windows Ghidra 분석은 사용할 수 없습니다. 프로세스 소유권, 비공개 디렉터리 권한, 안전한 경로 검사가 구현되지 않았으므로 Ghidra와 Java를 올바르게 설치해도 분석을 활성화할 수 없습니다. [Windows Ghidra P0](docs/windows-ghidra-p0.md)를 참고하세요.
|
||||
|
||||
### 문제 해결
|
||||
|
||||
`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)를 참고하세요.
|
||||
|
||||
### 업데이트와 제거
|
||||
|
||||
- `rea update`는 현재 REA 설치를 업데이트합니다.
|
||||
- `rea uninstall`은 REA가 관리하는 에이전트 등록과 워크플로 파일을 제거합니다. Hopper는 유지합니다.
|
||||
- `rea uninstall --purge-data`는 REA 캐시와 상태도 삭제합니다. 해당 데이터를 지우려는 경우에만 사용하세요.
|
||||
|
||||
## 현재 지원 범위
|
||||
|
||||
REA는 CLI와 MCP로 네이티브 바이너리, JavaScript/Electron 앱, .NET 어셈블리, 웹사이트를 분석합니다. 사용할 수 있는 작업은 호스트, 대상, 선택한 분석 도구에 따라 다릅니다.
|
||||
|
||||
- Hopper는 네이티브 분석과 주석 작업을 지원합니다. GUI 동작은 플랫폼에 따라 다릅니다.
|
||||
- Ghidra는 Linux x64와 macOS x64/arm64에서 목록, 검색, 디컴파일, 어셈블리, 호출, 참조, 명령어, 타입 검사 등 읽기 전용 작업 22개를 제공합니다. GUI나 변경 작업은 제공하지 않습니다.
|
||||
- 브라우저, Electron, 프로세스 런타임 워크플로에는 각각 설정, 승인, 종료 처리 요구 사항이 있습니다. 자세한 내용은 [English README](README.md#current-status)를 참고하세요.
|
||||
- Windows Ghidra 분석은 아직 사용할 수 없습니다. 진행 상황은 [#527](https://github.com/morluto/rea/issues/527)에서 확인하세요.
|
||||
|
||||
## 하나의 프롬프트로 전체 조사
|
||||
|
||||
```text
|
||||
@@ -149,22 +192,26 @@ REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이
|
||||
|
||||
## 조사 도구 카탈로그
|
||||
|
||||
| 도구 그룹 | 수 | 예시 |
|
||||
| ------------------- | --: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 바이너리 검사 | 36 | 프로시저, 의사 코드, 어셈블리, 문자열, 이름, 세그먼트, callers, callees, xrefs, 주석 |
|
||||
| 합성 분석 | 14 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, Swift 및 ObjC 탐색 |
|
||||
| macOS 네이티브 도구 | 5 | Mach-O 메타데이터, 코드 서명, plist, 아키텍처, Swift 디맹글링. Hopper 실행 불필요 |
|
||||
| 아티팩트 그래프 | 3 | 디렉터리와 지원 패키지의 전체 인라인 검사 및 명시적으로 선택한 트랜잭션 추출 |
|
||||
| Managed PE/CLI | 8 | PE/CLI ID, 메타데이터 멤버, CIL 해시, P/Invoke/네이티브 경계 선언 및 검증, 애플리케이션 그래프 투영, 디컴파일 재구성 가져오기, 구조적 token 재매핑, 런타임 상관 계획, 버전 비교 |
|
||||
| 브라우저 관찰 | 9 | origin 제한 CDP 캡처, bundle/source map 분석, WebMCP 검색, 세션 타임라인, capture diff 및 시각 증거 |
|
||||
| Electron 분석 | 5 | canonical 파일 루트 내 수동 관찰, 제한된 정적 앱 매핑, Evidence 기반 정적/런타임 조정 |
|
||||
| JavaScript 런타임 | 2 | 승인된 연결 전용 Node/Electron Inspector 대상 탐색과 평가·계측 없는 제한된 스크립트 및 실행 컨텍스트 관찰 |
|
||||
| 애플리케이션 | 10 | 제한된 교차 계층 추적, 고유 일치 버전 비교, 정적 export 반환 형태 비교, 승인된 Linux 격리 extracted-module replay, 관리형 런타임 특성화, 재구성 커버리지 폐쇄, 결정론적 의무 원장 |
|
||||
| 바이너리 세션 | 22 | `open_binary`, `binary_session`, 증거 번들, 유한 replay 상태 머신, 프로세스·아티팩트·함수 비교, 잔여 미지수 레지스트리 |
|
||||
| 도구 분류 | 수 | 용도 |
|
||||
| ----------------- | --: | -------------------------------------------------------------------- |
|
||||
| 바이너리 검사 | 41 | 함수, 의사 코드, 어셈블리, 문자열, 심볼, 참조, 주석 |
|
||||
| 결합 분석 | 14 | 개요, 함수 분석, 일괄 디컴파일, 호출 그래프, Swift와 ObjC 검사 |
|
||||
| macOS 네이티브 | 7 | Mach-O 메타데이터, 서명, plist, 아키텍처, Swift 이름 복원 |
|
||||
| 파일과 패키지 | 5 | 디렉터리와 패키지 검사, Interface Builder, Apple 리소스, 추출 |
|
||||
| .NET PE/CLI | 7 | 어셈블리 식별, 메타데이터, CIL, 네이티브 호출, 비교, 재구성 가져오기 |
|
||||
| 브라우저 관찰 | 9 | 페이지, 스크립트, 소스 맵, WebMCP, 스크린샷, 캡처 비교 |
|
||||
| Electron 분석 | 5 | 페이지 관찰, 앱 구조, 정적·런타임 결과 연결 |
|
||||
| JavaScript 런타임 | 2 | 기존 Node/Electron Inspector에 연결해 스크립트와 실행 컨텍스트 관찰 |
|
||||
| 앱 워크플로 | 7 | 기능 추적, 버전 비교, 반환 구조 비교, 재구현 검증 |
|
||||
| 바이너리 세션 | 21 | 대상 전환, 근거 저장, 프로세스·함수 비교, 미해결 항목 기록 |
|
||||
|
||||
## 로드맵
|
||||
|
||||
다음 작업은 네이티브 대상 검증 확대, JavaScript와 .NET의 버전 비교, 런타임 관찰과 재구현 검증입니다. Setup은 에이전트 연결과 Hopper 설치 선택을 이미 지원합니다. 다른 분석 도구 설치는 향후 작업입니다. [설치 로드맵](docs/roadmap.md)과 [분석 도구 평가](docs/provider-evaluation.md)를 참고하세요.
|
||||
|
||||
## 다른 코딩 에이전트에서 사용하기
|
||||
|
||||
Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin을 감지하고 앞의 여섯 클라이언트를 자동으로 구성합니다. Devin은 감지만 보고하고 변경하지 않습니다. 로컬 MCP 서버를 지원하는 코딩 에이전트라면 다음 설정으로 REA를 사용할 수 있습니다.
|
||||
Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity, GitHub Copilot CLI, Command Code, VS Code를 지원합니다. 기존 REA 등록은 기본 선택되고, 그 밖의 감지된 클라이언트는 직접 선택해야 합니다. 로컬 MCP 서버를 지원하는 에이전트는 다음 설정으로 연결할 수 있습니다.
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
@@ -173,7 +220,7 @@ Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@3.2.1", "mcp"]
|
||||
"args": ["-y", "rea-agents@4.0.1", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,7 +240,7 @@ flowchart LR
|
||||
Ghidra --> App
|
||||
```
|
||||
|
||||
CLI와 MCP 서버는 같은 분석 엔진을 사용합니다. 터미널 명령은 끝날 때 앱을 닫고, 에이전트 세션은 조사하는 동안 앱을 열어 둡니다.
|
||||
CLI와 MCP 서버는 같은 분석 워크플로를 사용합니다. 터미널 명령은 완료 후 자신의 브리지 세션을 해제하고, 에이전트 세션은 조사 중 연결을 유지할 수 있습니다. REA 세션을 닫아도 사용자가 이용 중인 Hopper 앱은 종료하지 않습니다.
|
||||
|
||||
## CLI
|
||||
|
||||
@@ -229,7 +276,7 @@ REA는 필요할 때 Hopper를 시작합니다. Hopper 런처는 내부적으로
|
||||
|
||||
<details><summary><strong>Hopper를 미리 실행해야 하나요?</strong></summary>
|
||||
|
||||
아니요. REA가 필요할 때 시작하며 이미 실행 중인 Hopper도 지원합니다.
|
||||
아니요. REA가 필요할 때 시작합니다. Hopper가 실행 중이어도 사용할 수 있지만 기존 GUI 문서에 연결하는 대신 새 분석 문서를 엽니다.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
+122
-76
@@ -4,7 +4,7 @@
|
||||
|
||||
# REA:逆向工程一切
|
||||
|
||||
### 一个 CLI 与 MCP 服务器,让编程智能体逆向工程任何程序
|
||||
### 使用智能体逆向工程任何程序,从应用行为到原生二进制文件。
|
||||
|
||||
**看到喜欢的功能。理解它的原理。按照你的方式实现。**
|
||||
|
||||
@@ -13,35 +13,51 @@
|
||||
[](#调查工具目录)
|
||||
[](https://nodejs.org/)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
[快速开始](#快速开始) · [从二进制到行为](#从二进制到行为) · [工具目录](#调查工具目录) · [工作原理](#工作原理) · [常见问题](#常见问题)
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/82054/weekly?language=TypeScript" alt="morluto/rea | Trendshift" width="250" height="55" /></a>
|
||||
|
||||
[快速开始](#快速开始) · [当前状态](#当前状态) · [从二进制到行为](#从二进制到行为) · [调查工具目录](#调查工具目录) · [路线图](#路线图) · [工作原理](#工作原理)
|
||||
|
||||
<table aria-label="REA community">
|
||||
<tr>
|
||||
<td align="center" width="360">
|
||||
<a href="https://discord.gg/GkcryMnJDM">
|
||||
<img src="docs/assets/discord.svg" height="42" alt="Discord" /><br />
|
||||
<strong>加入逆向工程社区</strong>
|
||||
</a><br />
|
||||
<sub>Discord · 问答 · 成果分享</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
<code>curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash</code>
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="REA 在 Hopper 中启动分析桥并检查原生二进制文件" width="1200" />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
看到某个应用中想加入自己产品的功能?即使没有源代码,也可以把应用交给编程智能体。借助 REA,智能体能够调查该功能、理解其工作原理,并按照你的技术栈、设计和需求构建适合你产品的版本。
|
||||
看到某个应用中想加入自己产品的功能?让智能体用 REA 调查它。即使没有源代码,智能体也可以检查应用、解释功能的工作方式、展示证据,并为你的项目实现类似功能。
|
||||
|
||||
REA 通过一个 CLI 与 MCP 服务器实现这套流程。智能体可以检查编译后的应用、追踪功能的工作方式,并把了解到的内容用于日常编码。REA 在一个接口背后处理复杂的逆向工程工具。
|
||||
REA 提供原生二进制、JavaScript 与 Electron 应用、.NET 程序集和网站的分析工具。你可以从智能体或终端使用它们。分析在本机运行,结果会说明证据和限制。
|
||||
|
||||
Setup 可以配置智能体、连接已安装的 Hopper 或 Ghidra,也可以在你批准后安装 Hopper。
|
||||
|
||||
## 直接询问智能体
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
然后说:
|
||||
完成[设置](#快速开始)后,重启智能体并提出问题:
|
||||
|
||||
```text
|
||||
逆向工程“备忘录”应用。解释搜索功能的工作方式,
|
||||
说明你是如何得出结论的,然后为我的项目构建类似功能。
|
||||
解释“备忘录”应用的搜索功能,展示证据,然后为我的项目实现类似功能。
|
||||
```
|
||||
|
||||
“备忘录”只是示例。你可以说出任何想了解的应用,也可以让智能体先从概览开始。
|
||||
把“备忘录”替换成你想了解的应用,也可以先请求概览。
|
||||
|
||||
## 从二进制到行为
|
||||
|
||||
@@ -53,70 +69,96 @@ REA 让调查始终以二进制证据为依据。它不会声称能恢复原始
|
||||
|
||||
## 为什么选择 REA
|
||||
|
||||
| | |
|
||||
| ---------------- | -------------------------------------------------------------- |
|
||||
| **为智能体设计** | 直接询问编译后应用的行为,让智能体搜集证据,而不是猜测。 |
|
||||
| **CLI 与 MCP** | 在终端或编程智能体中使用同一套逆向工程能力。 |
|
||||
| **处理复杂流程** | REA 负责工具设置、打开应用、维持调查过程,并在完成后清理资源。 |
|
||||
| **完整工作流** | 从初步概览推进到伪代码、调用关系、类型和实现线索。 |
|
||||
| **本地运行** | 分析在你的 Mac 上运行;REA 不会把二进制上传到托管式分析服务。 |
|
||||
| **保留上下文** | 连续调查多个二进制文件,无需为每个问题重新开始整个分析过程。 |
|
||||
| | |
|
||||
| ---------------- | -------------------------------------------------------------------- |
|
||||
| **为智能体设计** | 直接询问编译后应用的行为,让智能体搜集证据,而不是猜测。 |
|
||||
| **CLI 与 MCP** | 在终端或编程智能体中使用同一套逆向工程能力。 |
|
||||
| **处理复杂流程** | REA 负责工具设置、打开应用、维持调查过程,并在完成后清理资源。 |
|
||||
| **完整工作流** | 从初步概览推进到伪代码、调用关系、类型和实现线索。 |
|
||||
| **本地运行** | 分析在受支持的本机系统上运行;REA 不会把二进制上传到托管式分析服务。 |
|
||||
| **保留上下文** | 连续调查多个二进制文件,无需为每个问题重新开始整个分析过程。 |
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 使用编程智能体(推荐)
|
||||
### 运行设置(推荐)
|
||||
|
||||
配置 REA 与智能体的连接:
|
||||
|
||||
```bash
|
||||
npx --yes rea-agents@latest setup
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
让智能体设置 REA。它会检查你的 Mac,说明需要安装的内容,先征得同意,并引导你处理系统提示。如果智能体要求重启以加载完整工具,请在设置后重启。
|
||||
Setup 首先让你多选要连接的智能体。已有 REA 注册默认选中;仅被检测到的客户端不会自动选中,也可以手动选择尚未配置的客户端。查看具体路径和变更后再批准。所选智能体默认安装 REA 工作流;Hopper 是单独的可选操作,需要单独批准。Setup 也可以记录现有 Ghidra 的路径。
|
||||
|
||||
### 开始之前
|
||||
Setup 会先展示变更并备份已有配置。要求与更多选项见[安装与设置](docs/installation.md)。
|
||||
|
||||
- macOS 12 或更高版本
|
||||
- Ubuntu 24.04+、Fedora 41+ 或 64 位 Arch Linux
|
||||
- Node.js 22.19+ 或 24.11+,以及随 Node 安装的 npm
|
||||
### 使用智能体
|
||||
|
||||
`rea setup` 会先显示完整变更计划并请求确认。它不会安装或更新 Homebrew、Node.js 或 npm。缺少 [Hopper](https://www.hopperapp.com/) 时,Setup 会提议安装官方软件包;Hopper 是需要单独许可证的独立软件。
|
||||
设置完成后,重启智能体并描述你想了解的应用或功能。REA 支持 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code 和 VS Code。已有 REA 注册默认选中;其他检测到的客户端需手动选择。其他智能体可以使用下方的 MCP 配置。
|
||||
|
||||
如果你已在 64 位 Linux 上安装 Ghidra 12.1.4 PUBLIC 和完整的 64 位 JDK 21,Setup 也可以在批准后登记 `GHIDRA_INSTALL_DIR` 和可选的 `JAVA_HOME`。REA 不会下载、安装或修改 Ghidra 或 Java。Ghidra provider 在隔离的只读 headless 会话中提供清单、反编译、汇编、调用关系、带类型的引用、xref、CFG 与函数 dossier;GUI 状态和修改操作仍不可用。
|
||||
Hopper 支持演示模式。如果首次启动时出现提示,选择演示模式或输入已有许可证。
|
||||
|
||||
#### Linux 安装与故障排除
|
||||
### 从终端运行
|
||||
|
||||
在 Ubuntu 24.04+、Fedora 41+ 和 64 位 Arch Linux 上,REA 会下载 Hopper 官方的 DEB、RPM 或 Arch 软件包,验证发布方提供的大小和校验和,然后通过 `apt-get`、`dnf` 或 `pacman` 安装依赖。非 root 运行时,`pkexec` 会显示系统授权提示;REA 不调用 `sudo`。
|
||||
|
||||
默认启动器是 `/opt/hopper/bin/Hopper`。自定义安装可设置 `HOPPER_LAUNCHER_PATH`。如果 Doctor 报告缺少分析引擎,请运行 `ldd /opt/hopper/bin/Hopper | grep 'not found'`,安装缺少的系统库后重新运行 `rea setup`。REA 会在私有 Xvfb 显示器上运行受支持的 Hopper 演示版,并为每个分析会话选择 Hopper 提供的演示模式;无需桌面 `DISPLAY` 或付费许可证,但演示版受厂商限制。Linux 上还应确保 `~/.local/bin` 位于 `PATH` 中。
|
||||
|
||||
```bash
|
||||
# 1. 安装和配置 REA
|
||||
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
|
||||
npx --yes rea-agents@latest setup
|
||||
```
|
||||
|
||||
如果 macOS 或安装程序要求确认,请完成提示,然后再次运行同一命令。
|
||||
|
||||
### 2. 重启编程智能体
|
||||
|
||||
Setup 会检测 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf 和 Devin。它会自动配置检测到的前六种客户端;由于目前没有明确的本地 MCP 配置边界,检测到 Devin 时只会报告而不会修改。重启已配置的客户端,让它加载 REA。
|
||||
|
||||
### 3. 直接询问智能体
|
||||
|
||||
你可以直接说出应用名称。编程智能体会找到应用,并把 REA 所需的程序文件交给它。
|
||||
|
||||
```text
|
||||
使用 REA 逆向工程“备忘录”应用。解释搜索功能的工作方式,展示证据,
|
||||
然后使用 SQLite 为我的项目构建一个类似功能。
|
||||
```
|
||||
|
||||
如果遇到问题,请运行:
|
||||
设置完成后:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest doctor
|
||||
rea uninstall
|
||||
rea uninstall --purge-data
|
||||
npx -y rea-agents@latest analyze /Applications/Notes.app
|
||||
```
|
||||
|
||||
### 安装 rea 命令
|
||||
|
||||
安装命令行工具:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
|
||||
```
|
||||
|
||||
需要预先安装 Node.js 和 npm。在终端中运行时,安装程序会安装 `rea` 并启动设置。
|
||||
|
||||
也可以用 npm 安装,再运行设置:
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea setup
|
||||
```
|
||||
|
||||
### 运行要求
|
||||
|
||||
- macOS 12 或更高版本
|
||||
- Ubuntu 24.04+、Fedora 41+ 或 64 位 Arch Linux
|
||||
- Node.js 22.x (>=22.19)、24.x (>=24.11) 或 26+,以及 npm
|
||||
|
||||
原生二进制分析需要 Hopper 或 Ghidra。Hopper 是独立软件;演示模式有厂商规定的限制,不要求购买许可证。
|
||||
|
||||
Ghidra 支持 Linux x64 和 macOS x64/arm64。单独安装 Ghidra 12.1.4 和完整的 64 位 JDK 21,然后配置 REA 使用它们。macOS 还需要对应架构的原生反编译器。
|
||||
|
||||
Setup 可以验证安装并保存路径,不会安装或升级 Ghidra、Java、Node.js、npm 或 Homebrew。
|
||||
|
||||
Windows Ghidra 分析目前不可用。进程归属、私有目录权限和安全路径检查尚未实现,正确安装 Ghidra 和 Java 也不能启用分析。详见 [Windows Ghidra P0](docs/windows-ghidra-p0.md)。
|
||||
|
||||
### 故障排查
|
||||
|
||||
运行 `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)。
|
||||
|
||||
### 更新与卸载
|
||||
|
||||
- `rea update` 更新当前 REA 安装。
|
||||
- `rea uninstall` 移除 REA 管理的智能体配置项和工作流文件,保留 Hopper。
|
||||
- `rea uninstall --purge-data` 还会删除 REA 的缓存和状态。仅在需要移除这些数据时使用。
|
||||
|
||||
## 当前状态
|
||||
|
||||
REA 可以通过 CLI 和 MCP 分析原生二进制、JavaScript/Electron 应用、.NET 程序集和网站。具体操作取决于主机、目标和所选分析工具。
|
||||
|
||||
- Hopper 支持原生分析和注释操作;GUI 行为取决于平台。
|
||||
- Ghidra 在 Linux x64 和 macOS x64/arm64 上提供 22 项只读操作,包括清单、搜索、反编译、汇编、调用关系、引用、指令与类型检查。它不提供 GUI 或修改操作。
|
||||
- 浏览器、Electron 和进程运行时工作流有独立的配置、批准和生命周期要求。详见 [English README](README.md#current-status) 中的完整状态说明。
|
||||
- Windows Ghidra 分析尚未启用,跟踪进展见 [#527](https://github.com/morluto/rea/issues/527)。
|
||||
|
||||
## 一个提示词,完成一次完整调查
|
||||
|
||||
```text
|
||||
@@ -149,22 +191,26 @@ REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常
|
||||
|
||||
## 调查工具目录
|
||||
|
||||
| 工具类别 | 数量 | 示例 |
|
||||
| ----------------- | ---: | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 二进制检查 | 36 | 过程、伪代码、汇编、字符串、名称、段、调用者、被调用者、交叉引用、注释 |
|
||||
| 组合分析 | 14 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`、Swift 与 ObjC 发现 |
|
||||
| macOS 原生工具 | 5 | Mach-O 元数据、代码签名、plist、架构与 Swift 符号还原,无需启动 Hopper |
|
||||
| 制品图谱 | 3 | 目录及支持的软件包的完整内联检查,以及显式选择的事务式提取 |
|
||||
| Managed PE/CLI | 8 | PE/CLI 身份、元数据成员、CIL 哈希、P/Invoke/原生边界声明与验证、应用图谱投影、反编译重建导入、结构化 token 重映射、运行时关联计划与跨版本比较 |
|
||||
| 浏览器观察 | 9 | 精确来源 CDP 抓取、bundle/source map 分析、WebMCP 发现、会话时间线、capture diff 与视觉证据 |
|
||||
| Electron 分析 | 5 | 限定 canonical 文件根的被动观察、有界静态应用映射,以及基于 Evidence 的静态/运行时对账 |
|
||||
| JavaScript 运行时 | 2 | 经批准的只附加 Node/Electron Inspector 目标发现,以及不执行求值或插桩的有界脚本和执行上下文观察 |
|
||||
| 应用工作流 | 10 | 有界跨层追踪、唯一匹配跨版本图比较、静态导出返回形状比较、经批准的 Linux 隔离 extracted-module replay、托管运行时表征、重建覆盖闭合和确定性义务账本 |
|
||||
| 二进制会话 | 22 | `open_binary`、`binary_session`、证据包、有限重放状态机、进程、制品与函数比较、残余未知项注册表 |
|
||||
| 工具类别 | 数量 | 用途 |
|
||||
| ----------------- | ---: | --------------------------------------------------------- |
|
||||
| 二进制检查 | 41 | 函数、伪代码、汇编、字符串、符号、段、引用与注释 |
|
||||
| 组合分析 | 14 | 概览、函数分析、批量反编译、调用图、Swift 与 ObjC 检查 |
|
||||
| macOS 原生工具 | 7 | Mach-O 元数据、代码签名、plist、架构与 Swift 符号还原 |
|
||||
| 制品检查 | 5 | 目录与软件包检查、Interface Builder、Apple 资源目录与提取 |
|
||||
| .NET PE/CLI | 7 | 程序集身份、元数据、CIL、原生调用声明、成员比较与重建导入 |
|
||||
| 浏览器观察 | 9 | 页面、脚本、来源映射、WebMCP 工具、截图与捕获比较 |
|
||||
| Electron 分析 | 5 | 页面观察、应用结构与静态/运行时结果关联 |
|
||||
| JavaScript 运行时 | 2 | 连接现有 Node/Electron Inspector,观察脚本与执行上下文 |
|
||||
| 应用工作流 | 7 | 功能追踪、版本比较、返回结构比较与重建验证 |
|
||||
| 二进制会话 | 21 | 目标切换、证据包、进程与函数比较、待解决问题记录 |
|
||||
|
||||
## 路线图
|
||||
|
||||
接下来的工作包括更多原生目标验证、JavaScript 与 .NET 跨版本比较,以及运行时观察和重建验证。当前设置已支持选择智能体集成和 Hopper 安装;安装其他分析工具仍是后续工作。详见[安装路线图](docs/roadmap.md)和[分析工具评估](docs/provider-evaluation.md)。
|
||||
|
||||
## 与其他编程智能体一起使用
|
||||
|
||||
Setup 会检测 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf 和 Devin,并自动配置检测到的前六种客户端;检测到 Devin 时只会报告而不会修改。任何支持本地 MCP 服务器的编程智能体都可以使用以下配置连接 REA。
|
||||
Setup 支持 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf、Devin、OpenCode、Antigravity、GitHub Copilot CLI、Command Code 和 VS Code。已有 REA 注册默认选中;其他检测到的客户端需手动选择。任何支持本地 MCP 服务器的智能体都可以使用以下配置连接 REA。
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
@@ -173,7 +219,7 @@ Setup 会检测 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Win
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@3.2.1", "mcp"]
|
||||
"args": ["-y", "rea-agents@4.0.1", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,7 +239,7 @@ flowchart LR
|
||||
Ghidra --> App
|
||||
```
|
||||
|
||||
CLI 与 MCP 服务器使用相同的分析引擎。终端命令完成后会关闭应用;智能体会话则会在调查期间保持应用打开。
|
||||
CLI 与 MCP 服务器使用相同的分析流程。终端命令完成后释放自己创建的桥接会话;智能体会话可以在调查期间保持连接。关闭 REA 会话不会退出用户正在使用的 Hopper 应用。
|
||||
|
||||
## CLI
|
||||
|
||||
@@ -223,7 +269,7 @@ REA 会推导明确的格式和架构参数,以避免常见的 FAT 与 ARM 选
|
||||
|
||||
## 安全模型
|
||||
|
||||
每个桥会话都使用随机能力令牌和仅限当前用户的 Unix 套接字。协议消息有大小限制,面向调用者的错误不会暴露启动器 stderr 或内部异常原因。Ghidra 会话还使用隔离的临时项目,不会打开或修改用户自己的 Ghidra 项目。
|
||||
每个桥会话都使用随机能力令牌和仅限当前用户的 Unix 套接字。诊断保留本机路径、摘要和错误位置等排查信息,同时移除凭据和认证令牌。Ghidra 会话还使用隔离的临时项目,不会打开或修改用户自己的 Ghidra 项目。
|
||||
|
||||
这不是沙箱,也无法防御以同一操作系统用户身份运行的恶意进程。打开不可信二进制文件会让所选本地提供商以当前用户权限进行解析和分析。请按照 [SECURITY.md](SECURITY.md) 中的私密流程报告漏洞。
|
||||
|
||||
@@ -231,7 +277,7 @@ REA 会推导明确的格式和架构参数,以避免常见的 FAT 与 ARM 选
|
||||
|
||||
<details><summary><strong>Hopper 是否需要提前运行?</strong></summary>
|
||||
|
||||
不需要。REA 会在操作需要时启动 Hopper,也支持已经运行的 Hopper。
|
||||
不需要。REA 会在操作需要时启动 Hopper。Hopper 已经运行时也可以使用,但 REA 会打开新的分析文档,不会连接并复用现有 GUI 文档。
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
import ghidra.app.script.GhidraScript;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import java.math.BigInteger;
|
||||
|
||||
/** Seed the declared DOS COM analysis context before default auto-analysis. */
|
||||
public final class ReaGhidraPrepareCom extends GhidraScript {
|
||||
@Override
|
||||
public void run() throws Exception {
|
||||
if (!currentProgram.getLanguageID().getIdAsString().equals("x86:LE:16:Real Mode") ||
|
||||
!currentProgram.getExecutableFormat().equals("Raw Binary")) {
|
||||
throw new IllegalArgumentException("DOS COM preparation requires raw x86 real-mode import");
|
||||
}
|
||||
Address entry = currentProgram.getAddressFactory().getDefaultAddressSpace().getAddress("1000:0100");
|
||||
Address start = currentProgram.getMinAddress();
|
||||
Address end = currentProgram.getMaxAddress();
|
||||
if (!entry.equals(start) || end.subtract(start) + 1 > 0xff00) {
|
||||
throw new IllegalArgumentException("DOS COM import must map 1..65280 bytes at 1000:0100");
|
||||
}
|
||||
for (String name : new String[] { "CS", "DS", "ES", "SS" }) {
|
||||
monitor.checkCancelled();
|
||||
currentProgram.getProgramContext().setValue(
|
||||
currentProgram.getRegister(name), start, end, BigInteger.valueOf(0x1000)
|
||||
);
|
||||
}
|
||||
addEntryPoint(entry);
|
||||
if (!disassemble(entry) || createFunction(entry, "entry") == null) {
|
||||
throw new IllegalStateException("DOS COM entry cannot be decoded as a function");
|
||||
}
|
||||
currentProgram.getOptions(Program.PROGRAM_INFO).setBoolean("REA DOS COM prepared", true);
|
||||
}
|
||||
}
|
||||
+70
-27
@@ -128,6 +128,10 @@ class CapabilityUnavailableError(Exception):
|
||||
"""The active Hopper build lacks one admitted public API operation."""
|
||||
|
||||
|
||||
class InvalidRequestError(Exception):
|
||||
"""Caller input failed bridge request or operation validation."""
|
||||
|
||||
|
||||
EXHAUSTIVE_ANALYSIS_METHODS = frozenset(
|
||||
(
|
||||
"analyze_function",
|
||||
@@ -150,6 +154,11 @@ EXHAUSTIVE_ANALYSIS_METHODS = frozenset(
|
||||
|
||||
|
||||
def _hex(value):
|
||||
if not isinstance(value, int) or isinstance(value, bool):
|
||||
try:
|
||||
value = int(str(value), 16)
|
||||
except ValueError as error:
|
||||
raise ValueError("Hopper returned a non-hexadecimal address") from error
|
||||
return "0x%x" % value
|
||||
|
||||
|
||||
@@ -177,8 +186,8 @@ def _document(name=None):
|
||||
current = api.current_document()
|
||||
if current is not None and current.getDocumentName() == name:
|
||||
return current
|
||||
raise ValueError("Hopper document name is ambiguous")
|
||||
raise ValueError("Unknown Hopper document")
|
||||
raise InvalidRequestError("Hopper document name is ambiguous")
|
||||
raise InvalidRequestError("Unknown Hopper document")
|
||||
if _selected_document is not None:
|
||||
for candidate in documents:
|
||||
if candidate.getDocumentName() == _selected_document:
|
||||
@@ -188,7 +197,7 @@ def _document(name=None):
|
||||
return session_document
|
||||
current = api.current_document()
|
||||
if current is None:
|
||||
raise ValueError("No Hopper document is loaded")
|
||||
raise CapabilityUnavailableError("No Hopper document is loaded")
|
||||
return current
|
||||
|
||||
|
||||
@@ -197,20 +206,20 @@ def _address(document, value=None):
|
||||
if value is None:
|
||||
return document.getCurrentAddress()
|
||||
if not isinstance(value, str):
|
||||
raise ValueError("Address must be a string")
|
||||
raise InvalidRequestError("Address must be a string")
|
||||
try:
|
||||
return int(value, 16)
|
||||
except ValueError:
|
||||
result = document.getAddressForName(value)
|
||||
if result in BAD_ADDRESSES:
|
||||
raise ValueError("Unknown Hopper address or name")
|
||||
raise InvalidRequestError("Unknown Hopper address or name")
|
||||
return result
|
||||
|
||||
|
||||
def _segment(document, address):
|
||||
result = document.getSegmentAtAddress(address)
|
||||
if result is None:
|
||||
raise ValueError("Address is outside every segment")
|
||||
raise InvalidRequestError("Address is outside every segment")
|
||||
return result
|
||||
|
||||
|
||||
@@ -218,7 +227,7 @@ def _procedure(document, value=None):
|
||||
address = document.getCurrentAddress() if value is None else _address(document, value)
|
||||
result = _segment(document, address).getProcedureAtAddress(address)
|
||||
if result is None:
|
||||
raise ValueError("No procedure exists at the requested address")
|
||||
raise InvalidRequestError("No procedure exists at the requested address")
|
||||
return result
|
||||
|
||||
|
||||
@@ -232,6 +241,10 @@ def _procedure_identity(procedure):
|
||||
"address": _hex(procedure.getEntryPoint()),
|
||||
"name": _procedure_name(procedure),
|
||||
"classification": None,
|
||||
"body": {
|
||||
"available": False,
|
||||
"reason": "Hopper's public Python API does not expose complete function body ranges",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -278,7 +291,7 @@ def _procedure_references(document, params):
|
||||
procedure = _procedure(document, params.get("procedure"))
|
||||
direction = params.get("direction", "outgoing")
|
||||
if direction not in ("incoming", "outgoing"):
|
||||
raise ValueError("direction must be incoming or outgoing")
|
||||
raise InvalidRequestError("direction must be incoming or outgoing")
|
||||
addresses = _instruction_addresses(procedure)
|
||||
edges = set()
|
||||
for address in addresses:
|
||||
@@ -300,6 +313,12 @@ def _procedure_references(document, params):
|
||||
})
|
||||
return {
|
||||
"procedure": _procedure_identity(procedure), "direction": direction,
|
||||
"reference_kinds_available": False,
|
||||
# The public Hopper API returns observed references but does not expose
|
||||
# enough flow metadata to enumerate calls with no resolved target.
|
||||
# Keep the required list field explicit; the provider records this as
|
||||
# unknown coverage rather than claiming the list is exhaustive.
|
||||
"unresolved_calls": [],
|
||||
"references": items,
|
||||
}
|
||||
|
||||
@@ -366,13 +385,13 @@ def _search_inventory(document, kind):
|
||||
def _search_results(document, kind, params):
|
||||
pattern = params.get("pattern")
|
||||
if not isinstance(pattern, str) or not pattern:
|
||||
raise ValueError("pattern must be a non-empty string")
|
||||
raise InvalidRequestError("pattern must be a non-empty string")
|
||||
mode = params.get("mode", "literal")
|
||||
if mode not in ("literal", "regex"):
|
||||
raise ValueError("mode must be literal or regex")
|
||||
raise InvalidRequestError("mode must be literal or regex")
|
||||
case_sensitive = params.get("case_sensitive", False)
|
||||
if not isinstance(case_sensitive, bool):
|
||||
raise ValueError("case_sensitive must be a boolean")
|
||||
raise InvalidRequestError("case_sensitive must be a boolean")
|
||||
|
||||
if mode == "literal":
|
||||
needle = pattern if case_sensitive else pattern.casefold()
|
||||
@@ -381,7 +400,7 @@ def _search_results(document, kind, params):
|
||||
try:
|
||||
expression = re.compile(pattern, 0 if case_sensitive else re.IGNORECASE)
|
||||
except (re.error, OverflowError) as error:
|
||||
raise ValueError("Invalid regex pattern") from error
|
||||
raise InvalidRequestError("Invalid regex pattern") from error
|
||||
matches = lambda value: expression.search(value) is not None
|
||||
|
||||
selected = []
|
||||
@@ -544,7 +563,11 @@ def _analyze_function(document, params):
|
||||
referenced_strings.sort(key=lambda item: (int(item["address"], 16), int(item["source_address"], 16)))
|
||||
referenced_names.sort(key=lambda item: (int(item["address"], 16), int(item["source_address"], 16)))
|
||||
return {
|
||||
"procedure": {"address": _hex(procedure.getEntryPoint()), "name": _procedure_name(procedure), "classification": None, "signature": procedure.signatureString(), "locals": _procedure_locals(procedure)},
|
||||
"procedure": {
|
||||
**_procedure_identity(procedure),
|
||||
"signature": procedure.signatureString(),
|
||||
"locals": _procedure_locals(procedure),
|
||||
},
|
||||
"pseudocode": pseudo,
|
||||
"assembly": assembly_lines,
|
||||
"comments": comments,
|
||||
@@ -618,9 +641,9 @@ def _dispatch(method, params):
|
||||
if method == "read_bytes":
|
||||
length = params.get("length", 256)
|
||||
if isinstance(length, bool) or not isinstance(length, int):
|
||||
raise ValueError("Byte-read length must be an integer")
|
||||
raise InvalidRequestError("Byte-read length must be an integer")
|
||||
if length < 1:
|
||||
raise ValueError("Byte-read length must be at least 1")
|
||||
raise InvalidRequestError("Byte-read length must be at least 1")
|
||||
reader = getattr(document, "readBytes", None)
|
||||
if not callable(reader):
|
||||
raise CapabilityUnavailableError(
|
||||
@@ -654,7 +677,7 @@ def _dispatch(method, params):
|
||||
or offset in BAD_ADDRESSES
|
||||
or offset < 0
|
||||
):
|
||||
raise ValueError("Address has no authoritative file-offset mapping")
|
||||
raise InvalidRequestError("Address has no authoritative file-offset mapping")
|
||||
return {"address": _hex(address), "file_offset": offset}
|
||||
if method == "resolve_containing_procedure":
|
||||
address = _address(document, params.get("address"))
|
||||
@@ -690,7 +713,7 @@ def _dispatch(method, params):
|
||||
else:
|
||||
result = document.getInstructionStart(max(0, target - 1))
|
||||
if result in BAD_ADDRESSES:
|
||||
raise ValueError("No adjacent address")
|
||||
raise InvalidRequestError("No adjacent address")
|
||||
return _hex(result)
|
||||
if method == "list_segments":
|
||||
result = []
|
||||
@@ -729,14 +752,14 @@ def _dispatch(method, params):
|
||||
if requested is not None:
|
||||
key = _hex(_address(document, requested))
|
||||
values = {key: values[key]} if key in values else {}
|
||||
return [{"address": _hex(address), "value": value} for address, value in sorted(values.items())]
|
||||
return [{"address": address, "value": value} for address, value in values.items()]
|
||||
if method == "list_names":
|
||||
result = dict(_search_inventory(document, "name"))
|
||||
requested = params.get("address")
|
||||
if requested is not None:
|
||||
key = _hex(_address(document, requested))
|
||||
result = {key: result[key]} if key in result else {}
|
||||
return [{"address": _hex(address), "value": value} for address, value in sorted(result.items(), key=lambda item: int(item[0]))]
|
||||
return [{"address": address, "value": value} for address, value in result.items()]
|
||||
if method in ("search_procedures", "search_strings"):
|
||||
kind = "procedure" if method == "search_procedures" else "string"
|
||||
return _search_results(document, kind, params)
|
||||
@@ -755,7 +778,16 @@ def _dispatch(method, params):
|
||||
if method == "procedure_info":
|
||||
blocks = list(procedure.basicBlockIterator())
|
||||
length = sum(max(0, block.getEndingAddress() - block.getStartingAddress()) for block in blocks)
|
||||
return {"name": _procedure_name(procedure), "entrypoint": _hex(procedure.getEntryPoint()), "basicblock_count": procedure.getBasicBlockCount(), "length": length, "signature": procedure.signatureString(), "locals": _procedure_locals(procedure), "classification": None}
|
||||
return {
|
||||
"name": _procedure_name(procedure),
|
||||
"entrypoint": _hex(procedure.getEntryPoint()),
|
||||
"basicblock_count": procedure.getBasicBlockCount(),
|
||||
"length": length,
|
||||
"signature": procedure.signatureString(),
|
||||
"locals": _procedure_locals(procedure),
|
||||
"classification": None,
|
||||
"body": _procedure_identity(procedure)["body"],
|
||||
}
|
||||
if method == "set_address_name":
|
||||
address = _address(document, params.get("address"))
|
||||
try:
|
||||
@@ -789,7 +821,7 @@ def _dispatch(method, params):
|
||||
address = _address(document, params.get("address"))
|
||||
document.removeBookmarkAtAddress(address)
|
||||
return not document.hasBookmarkAtAddress(address)
|
||||
raise ValueError("Unknown bridge method")
|
||||
raise InvalidRequestError("Unknown bridge method")
|
||||
|
||||
|
||||
def _serve_connection(connection):
|
||||
@@ -803,13 +835,24 @@ def _serve_connection(connection):
|
||||
authenticated = False
|
||||
should_stop = False
|
||||
try:
|
||||
request = json.loads(line.decode("utf-8"))
|
||||
if set(request) != {"id", "token", "method", "params"}:
|
||||
raise ValueError("Invalid bridge request shape")
|
||||
try:
|
||||
request = json.loads(line.decode("utf-8"))
|
||||
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
||||
raise InvalidRequestError("Invalid bridge request JSON") from error
|
||||
if not isinstance(request, dict) or set(request) != {"id", "token", "method", "params"}:
|
||||
raise InvalidRequestError("Invalid bridge request shape")
|
||||
request_id = request["id"]
|
||||
if isinstance(request_id, bool) or not isinstance(request_id, int) or request_id < 0:
|
||||
raise ValueError("Invalid bridge request id")
|
||||
if not isinstance(request["token"], str) or not hmac.compare_digest(request["token"], REA_TOKEN):
|
||||
raise InvalidRequestError("Invalid bridge request id")
|
||||
if (
|
||||
not isinstance(request["method"], str) or not request["method"]
|
||||
or not isinstance(request["params"], dict)
|
||||
):
|
||||
raise InvalidRequestError("Invalid bridge method or parameters")
|
||||
if (
|
||||
not isinstance(request["token"], str) or not request["token"].isascii()
|
||||
or not hmac.compare_digest(request["token"], REA_TOKEN)
|
||||
):
|
||||
raise PermissionError("Invalid bridge capability")
|
||||
authenticated = True
|
||||
_write_message(file, {"id": request_id, "event": {
|
||||
@@ -864,7 +907,7 @@ def _diagnostic_type(error):
|
||||
return "capability_unavailable"
|
||||
if isinstance(error, PermissionError):
|
||||
return "authorization"
|
||||
if isinstance(error, (ValueError, TypeError, KeyError)):
|
||||
if isinstance(error, InvalidRequestError):
|
||||
return "invalid_request"
|
||||
return "bridge_exception"
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import AppKit
|
||||
import ApplicationServices
|
||||
import CryptoKit
|
||||
import ScreenCaptureKit
|
||||
import Darwin
|
||||
|
||||
struct Request: Decodable {
|
||||
var pid: Int32
|
||||
var window_id: UInt32
|
||||
var executable: String
|
||||
var sha256: String
|
||||
var launch_time: Double?
|
||||
var screenshot: Bool
|
||||
var accessibility: Bool
|
||||
var max_nodes: Int
|
||||
var action: Action?
|
||||
}
|
||||
struct Action: Decodable { var kind: String; var path: [Int]?; var direction: String?; var text: String? }
|
||||
struct BoundaryFailure: Error { var code: String; var message: String }
|
||||
func fail(_ code: String, _ message: String) throws -> Never { throw BoundaryFailure(code: code, message: message) }
|
||||
func attribute(_ element: AXUIElement, _ key: String) -> CFTypeRef? {
|
||||
var value: CFTypeRef?
|
||||
return AXUIElementCopyAttributeValue(element, key as CFString, &value) == .success ? value : nil
|
||||
}
|
||||
func childCount(_ element: AXUIElement) -> Int {
|
||||
var count = 0
|
||||
return AXUIElementGetAttributeValueCount(element, kAXChildrenAttribute as CFString, &count) == .success ? count : 0
|
||||
}
|
||||
func children(_ element: AXUIElement, limit: Int) -> [AXUIElement] {
|
||||
var value: CFArray?
|
||||
let count = min(limit, childCount(element))
|
||||
if count == 0 { return [] }
|
||||
return AXUIElementCopyAttributeValues(element, kAXChildrenAttribute as CFString, 0, count, &value) == .success ? value as? [AXUIElement] ?? [] : []
|
||||
}
|
||||
func text(_ element: AXUIElement, _ key: String) -> Any { (attribute(element, key) as? String) ?? NSNull() as Any }
|
||||
func windowBounds(_ element: AXUIElement) -> CGRect? {
|
||||
guard let p = attribute(element, kAXPositionAttribute), let s = attribute(element, kAXSizeAttribute),
|
||||
CFGetTypeID(p) == AXValueGetTypeID(), CFGetTypeID(s) == AXValueGetTypeID() else { return nil }
|
||||
var point = CGPoint.zero; var size = CGSize.zero
|
||||
guard AXValueGetValue(p as! AXValue, .cgPoint, &point), AXValueGetValue(s as! AXValue, .cgSize, &size) else { return nil }
|
||||
return CGRect(origin: point, size: size)
|
||||
}
|
||||
func observe(_ request: Request) async throws -> [String: Any] {
|
||||
guard let app = NSRunningApplication(processIdentifier: request.pid), let url = app.executableURL else {
|
||||
try fail("target-mismatch", "Selected PID does not expose an application executable URL")
|
||||
}
|
||||
guard let canonicalPointer = realpath(url.path, nil) else { try fail("target-mismatch", "Running executable path cannot be resolved") }
|
||||
defer { free(canonicalPointer) }
|
||||
let canonicalExecutable = String(cString: canonicalPointer)
|
||||
guard canonicalExecutable == request.executable else {
|
||||
try fail("target-mismatch", "Selected PID executable \(canonicalExecutable) differs from active target \(request.executable)")
|
||||
}
|
||||
guard let launch = app.launchDate?.timeIntervalSince1970 else {
|
||||
try fail("process-identity-unavailable", "Selected application does not expose a launch time; stable PID reuse checks are unavailable")
|
||||
}
|
||||
if let expected = request.launch_time, expected != launch { try fail("process-replaced", "Selected process was replaced") }
|
||||
let executableSize = (try FileManager.default.attributesOfItem(atPath: url.path)[.size] as? NSNumber)?.int64Value ?? -1
|
||||
guard executableSize >= 0 && executableSize <= 256 * 1024 * 1024 else { try fail("target-limit", "Running executable exceeds the 256 MiB identity-check budget") }
|
||||
let executableBytes = try Data(contentsOf: url, options: .mappedIfSafe)
|
||||
guard SHA256.hash(data: executableBytes).map({ String(format: "%02x", $0) }).joined() == request.sha256 else {
|
||||
try fail("target-mismatch", "Running executable bytes differ from the active target digest")
|
||||
}
|
||||
guard let windows = CGWindowListCopyWindowInfo([.optionIncludingWindow], request.window_id) as? [[String: Any]],
|
||||
windows.count == 1, let window = windows.first,
|
||||
(window[kCGWindowOwnerPID as String] as? NSNumber)?.int32Value == request.pid,
|
||||
(window[kCGWindowLayer as String] as? NSNumber)?.intValue == 0,
|
||||
let boundsDictionary = window[kCGWindowBounds as String] as? [String: Any],
|
||||
let bounds = CGRect(dictionaryRepresentation: boundsDictionary as CFDictionary) else {
|
||||
try fail("window-mismatch", "Selected window is absent or belongs to another process")
|
||||
}
|
||||
if request.screenshot && !CGPreflightScreenCaptureAccess() {
|
||||
try fail("screen-recording-denied", "Grant Screen Recording to the REA host in System Settings > Privacy & Security, then retry; no broad capture fallback is used")
|
||||
}
|
||||
var selected: AXUIElement?
|
||||
if request.accessibility || request.action != nil {
|
||||
guard AXIsProcessTrusted() else { try fail("accessibility-denied", "Grant Accessibility to the REA host in System Settings > Privacy & Security, then retry") }
|
||||
let application = AXUIElementCreateApplication(request.pid)
|
||||
AXUIElementSetMessagingTimeout(application, 2)
|
||||
guard let axWindows = attribute(application, kAXWindowsAttribute) as? [AXUIElement] else {
|
||||
try fail("accessibility-unavailable", "Selected application does not expose accessibility windows")
|
||||
}
|
||||
let matches = axWindows.filter { windowBounds($0) == bounds }
|
||||
guard matches.count == 1 else { try fail("ambiguous-window", "Accessibility window geometry does not identify exactly one selected window; selected CG bounds \(bounds), AX candidates \(axWindows.map { String(describing: windowBounds($0)) })") }
|
||||
selected = matches[0]
|
||||
}
|
||||
if let action = request.action, let root = selected {
|
||||
var element = root
|
||||
for index in action.path ?? [] {
|
||||
var selectedChild: CFArray?
|
||||
guard index >= 0 && index < childCount(element),
|
||||
AXUIElementCopyAttributeValues(element, kAXChildrenAttribute as CFString, index, 1, &selectedChild) == .success,
|
||||
let item = (selectedChild as? [AXUIElement])?.first else { try fail("element-missing", "Selected accessibility path no longer exists") }
|
||||
element = item
|
||||
}
|
||||
var owner: pid_t = 0
|
||||
guard AXUIElementGetPid(element, &owner) == .success && owner == request.pid else { try fail("element-owner-mismatch", "Selected element belongs to another process") }
|
||||
let outcome: AXError
|
||||
switch action.kind {
|
||||
case "click": outcome = AXUIElementPerformAction(element, kAXPressAction as CFString)
|
||||
case "scroll": outcome = AXUIElementPerformAction(element, (action.direction == "increment" ? kAXIncrementAction : kAXDecrementAction) as CFString)
|
||||
case "key-entry": outcome = AXUIElementSetAttributeValue(element, kAXValueAttribute as CFString, (action.text ?? "") as CFString)
|
||||
default: try fail("unsupported-action", "Only selected-element press, increment/decrement and text-value entry are admitted")
|
||||
}
|
||||
guard outcome == .success else { try fail("action-failed", "Accessibility action failed with AXError \(outcome.rawValue)") }
|
||||
}
|
||||
var nodes: [[String: Any]] = []; var truncated = false
|
||||
if request.accessibility, let root = selected {
|
||||
var pending: [(AXUIElement, [Int])] = [(root, [])]
|
||||
while let (element, path) = pending.popLast() {
|
||||
if nodes.count >= request.max_nodes { truncated = true; break }
|
||||
let totalChildren = childCount(element)
|
||||
var actions: CFArray?
|
||||
AXUIElementCopyActionNames(element, &actions)
|
||||
nodes.append(["path": path, "role": text(element, kAXRoleAttribute), "title": text(element, kAXTitleAttribute), "value": text(element, kAXValueAttribute), "actions": actions as? [String] ?? [], "children_count": totalChildren])
|
||||
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)
|
||||
if totalChildren > count { truncated = true }
|
||||
for index in (0..<count).reversed() { pending.append((items[index], path + [index])) }
|
||||
}
|
||||
}
|
||||
var screenshot: Any = NSNull()
|
||||
if request.screenshot {
|
||||
guard #available(macOS 14, *) else { try fail("unsupported-host", "Selected-window screenshots require macOS 14 or later") }
|
||||
let content = try await SCShareableContent.excludingDesktopWindows(true, onScreenWindowsOnly: false)
|
||||
guard let scWindow = content.windows.first(where: { $0.windowID == request.window_id && $0.owningApplication?.processID == request.pid }) else {
|
||||
try fail("capture-window-missing", "Selected window disappeared before capture")
|
||||
}
|
||||
guard bounds.width > 0 && bounds.height > 0 && bounds.width <= 100000 && bounds.height <= 100000 else { try fail("window-bounds", "Selected window has invalid or oversized geometry") }
|
||||
let filter = SCContentFilter(desktopIndependentWindow: scWindow)
|
||||
let configuration = SCStreamConfiguration()
|
||||
let scale = min(1, 2048 / max(bounds.width, bounds.height))
|
||||
configuration.width = max(1, Int(bounds.width * scale)); configuration.height = max(1, Int(bounds.height * scale))
|
||||
configuration.showsCursor = false
|
||||
let image = try await SCScreenshotManager.captureImage(contentFilter: filter, configuration: configuration)
|
||||
guard let png = NSBitmapImageRep(cgImage: image).representation(using: .png, properties: [:]) else {
|
||||
try fail("capture-failed", "Selected-window PNG encoding failed")
|
||||
}
|
||||
screenshot = ["mime_type": "image/png", "base64": png.base64EncodedString(), "sha256": SHA256.hash(data: png).map({ String(format: "%02x", $0) }).joined(), "width": image.width, "height": image.height]
|
||||
}
|
||||
return ["window": ["pid": request.pid, "window_id": request.window_id, "executable": request.executable, "launch_time": launch, "title": window[kCGWindowName as String] as? String ?? ""], "nodes": nodes, "truncated": truncated, "screenshot": screenshot, "gaps": request.accessibility ? [] : ["Accessibility capture was disabled"]]
|
||||
}
|
||||
Task { @MainActor in
|
||||
do {
|
||||
let request = try JSONDecoder().decode(Request.self, from: Data(CommandLine.arguments[1].utf8))
|
||||
let result = try await observe(request)
|
||||
let output = try JSONSerialization.data(withJSONObject: ["ok": true, "result": result], options: [.sortedKeys])
|
||||
FileHandle.standardOutput.write(output)
|
||||
} catch {
|
||||
let failure = error as? BoundaryFailure
|
||||
let output: [String: Any] = ["ok": false, "code": failure?.code ?? "capture-failed", "message": failure?.message ?? error.localizedDescription]
|
||||
if let data = try? JSONSerialization.data(withJSONObject: output, options: [.sortedKeys]) { FileHandle.standardOutput.write(data) }
|
||||
}
|
||||
exit(0)
|
||||
}
|
||||
RunLoop.main.run()
|
||||
@@ -6,7 +6,7 @@
|
||||
target binding, analysis-profile commitment, and snapshot/Evidence migration
|
||||
are implemented. Ghidra discovery, target/profile resolution, doctor checks,
|
||||
the private headless-session foundation, ten read-only inventory capabilities,
|
||||
and nine function-analysis capabilities are implemented with real
|
||||
and twelve function-analysis capabilities are implemented with real
|
||||
cross-format conformance.
|
||||
|
||||
## Context
|
||||
@@ -569,5 +569,7 @@ The first six implementation stages are shipped:
|
||||
|
||||
Future stages may deepen format and indirect-flow coverage, but must continue
|
||||
to compare normalized semantics rather than provider-specific pseudocode or
|
||||
assembly text. Ghidra continues to omit GUI and mutation operations, so
|
||||
unsupported requests cannot route to an unverified implementation.
|
||||
assembly text. Linux and macOS now admit atomic function-name and entry-comment
|
||||
edits in the ephemeral database, with readback, rollback, and cache invalidation.
|
||||
Executable bytes remain unchanged. Windows P0 retains its read-only boundary;
|
||||
GUI and other unimplemented mutations remain unavailable.
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
# ADR-0002: Controlled JavaScript replay authority and sandbox policy
|
||||
|
||||
- Status: Accepted
|
||||
- Status: Superseded; the controlled JavaScript replay tool was removed
|
||||
- Date: 2026-07-16
|
||||
- Implementation status: Implemented by `run_controlled_replay` with a
|
||||
Linux-x86_64 Bubblewrap, seccomp, and delegated systemd user-cgroup backend.
|
||||
Hosts that cannot establish every required boundary fail closed.
|
||||
- Implementation status: The implementation was removed. The design below is
|
||||
retained as historical context and does not describe a supported REA tool.
|
||||
|
||||
> Historical design note: The tool, sandbox backend, and plan/execute workflow
|
||||
> described here are no longer part of REA. For runtime behavior, run probes
|
||||
> directly against the application and use the available browser, Electron, or
|
||||
> process capture workflows.
|
||||
|
||||
## Context
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
# ADR-0003: Managed-code evidence and provider boundary
|
||||
|
||||
- Status: Accepted
|
||||
> Historical design record: The non-executing runtime-correlation planning
|
||||
> tool described in earlier revisions was removed. This ADR's current
|
||||
> implementation status below covers only the static managed tools that ship.
|
||||
|
||||
- Status: Superseded for runtime planning; static evidence boundary remains in force
|
||||
- Date: 2026-07-16
|
||||
- Implementation status: Read-only PE/CLI triage and exact identity are shipped
|
||||
through `inspect_managed_artifact` / `rea inspect-managed-artifact`. Bounded
|
||||
metadata, signatures, method bodies, raw CIL hashes, limited
|
||||
decoded-instruction-tuple v1 hashes, exception regions, call edges, and
|
||||
field-access anchors are shipped through `inspect_managed_members`; `rea
|
||||
inspect-managed-members` provides CLI parity. Static member comparison and build-local
|
||||
inspect-managed-members` provides CLI parity. Static member comparison and build-local
|
||||
token remapping are shipped through `compare_managed_members` /
|
||||
`rea compare-managed-members`. Declaration-only managed/native boundary
|
||||
inventory for ModuleRef, ImplMap/PInvoke, ReadyToRun indicators, and non-IL
|
||||
@@ -19,11 +23,9 @@ inspect-managed-members` provides CLI parity. Static member comparison and build
|
||||
addresses. Decompiler reconstruction import is
|
||||
shipped through `import_managed_reconstruction` /
|
||||
`rea import-managed-reconstruction`; it records user-supplied C#/IL/pseudocode
|
||||
as analyst inference only after exact static member locks match. Default-disabled
|
||||
runtime-correlation admission planning is shipped through
|
||||
`plan_managed_runtime_correlation` / `rea plan-managed-runtime-correlation`;
|
||||
it does not attach, load, debug, reflect, instrument, invoke, or execute
|
||||
target code. Managed static graph projection is shipped through
|
||||
as analyst inference only after exact static member locks match. Managed
|
||||
inspection does not execute assemblies or observe CLR internals. Managed static graph
|
||||
projection is shipped through
|
||||
`project_managed_application_graph` /
|
||||
`rea project-managed-application-graph`; it projects authenticated managed
|
||||
Evidence into the application graph without changing managed tokens into
|
||||
@@ -50,7 +52,7 @@ makes names especially weak while tokens remain coordinates in only one build.
|
||||
REA already separates one selected deep native provider from disjoint auxiliary
|
||||
providers under [ADR-0001](0001-provider-selection-and-analysis-profiles.md).
|
||||
It also separates static evidence, passive observation, process capture, and
|
||||
controlled replay authorities. Managed analysis must fit those boundaries
|
||||
interactive observation authorities. Managed analysis must fit those boundaries
|
||||
without requiring a CLR to inspect an artifact, loading target code into the
|
||||
REA process, or treating a managed token as a native address.
|
||||
|
||||
@@ -257,10 +259,13 @@ exception/local commitments, and an explicit algorithm version require a new
|
||||
normalized-CIL schema.
|
||||
|
||||
Metadata tokens are reported because they are precise coordinates inside one
|
||||
module. Cross-version identity never consists of a token alone. A structural
|
||||
match cites both build commitments, both local tokens, the v1 tuple inputs, and
|
||||
all competing candidates. V1 cannot cite a
|
||||
separate matching-algorithm version because none is exposed.
|
||||
module. Cross-version identity never consists of a token alone. Matching tries
|
||||
exact CIL/signature identity, then an exact declared-type/name/signature tuple,
|
||||
then structural shape; a name by itself never selects a pair. Partial or
|
||||
malformed bodies leave body facets unknown after a signature tuple pairs the
|
||||
methods. Matches cite both build commitments, both local tokens, and all
|
||||
competing candidates. V1 cannot cite a separate matching-algorithm version
|
||||
because none is exposed.
|
||||
|
||||
### 5. Separate five evidence layers
|
||||
|
||||
@@ -356,24 +361,13 @@ emit full IL, decompiled source, runtime logs, user data, credentials, or
|
||||
service/account material. No proprietary binary, hash-locked application
|
||||
manifest, or derived dump is committed to REA.
|
||||
|
||||
### 8. Keep runtime correlation a different, default-disabled authority
|
||||
### 8. Keep runtime behavior separate from static managed analysis
|
||||
|
||||
Static managed support grants no permission to attach, load, debug, reflect,
|
||||
instrument, invoke, or execute. The shipped runtime-correlation planning
|
||||
capability distinguishes those effects, remains disabled by default, requires
|
||||
explicit administrator and per-call approval, and binds to the exact artifact
|
||||
SHA-256, MVID, method signature, and CIL/body shape observed by the static
|
||||
path. It records an admission plan only; a future executor must pass a separate
|
||||
threat model before any effect can occur.
|
||||
|
||||
Runtime admission must additionally validate host OS, CLR family, architecture,
|
||||
and supported build/tool versions; bound time, threads, outputs, UI and network
|
||||
effects; avoid real services and accounts; and own all created processes and
|
||||
artifacts through complete cleanup. Reflection-only loading is still a CLR load
|
||||
and is not a substitute for the static byte parser.
|
||||
|
||||
Runtime execution is intentionally outside the current managed-code track and
|
||||
requires its own threat model and admission decision.
|
||||
No managed runtime planning or execution tool ships. Managed inspection does
|
||||
not execute assemblies or establish which methods ran. Where supported, direct
|
||||
process capture can record target-level inputs and outputs, but it does not
|
||||
prove CLR method execution or resolution. Internal managed behavior remains
|
||||
unknown until independently observed with suitable runtime evidence.
|
||||
|
||||
## Public contract consequences
|
||||
|
||||
@@ -440,9 +434,7 @@ or development-only oracles.
|
||||
5. Add source-built, pinned real-tool, package, CLI, and MCP conformance.
|
||||
Source-owned conformance is shipped; pinned external real-tool checks remain
|
||||
planned.
|
||||
6. Add separately authorized runtime-correlation admission planning. Shipped;
|
||||
runtime execution remains planned.
|
||||
7. Project authenticated managed static Evidence into the application graph.
|
||||
6. Project authenticated managed static Evidence into the application graph.
|
||||
Shipped.
|
||||
|
||||
Every gate must keep unsupported ReadyToRun, NativeAOT, IL2CPP, mixed-mode, or
|
||||
|
||||
+5
-5
@@ -4,8 +4,8 @@ Accepted decisions describe the architecture that implementation PRs must
|
||||
follow. Acceptance does not by itself mean the behavior is shipped; each record
|
||||
states its implementation status separately.
|
||||
|
||||
| ADR | Status | Implementation |
|
||||
| ------------------------------------------------------------------------------------------------------------------ | -------- | ----------------------------------------------- |
|
||||
| [0001: Provider selection and analysis profiles](0001-provider-selection-and-analysis-profiles.md) | Accepted | Foundation shipped; capability work continues |
|
||||
| [0002: Controlled JavaScript replay authority and sandbox policy](0002-controlled-replay-authority-and-sandbox.md) | Accepted | Linux x86_64 extracted-module replay shipped |
|
||||
| [0003: Managed-code evidence and provider boundary](0003-managed-code-evidence-and-provider-boundary.md) | Accepted | PE/CLI triage shipped; deeper contracts planned |
|
||||
| ADR | Status | Implementation |
|
||||
| ------------------------------------------------------------------------------------------------------------------ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [0001: Provider selection and analysis profiles](0001-provider-selection-and-analysis-profiles.md) | Accepted | Provider selection and 22 Ghidra operations shipped on Linux/macOS; experimental Windows x64 P0 with bundled native controls |
|
||||
| [0002: Controlled JavaScript replay authority and sandbox policy](0002-controlled-replay-authority-and-sandbox.md) | Superseded | Historical design; the controlled JavaScript replay tool was removed |
|
||||
| [0003: Managed-code evidence and provider boundary](0003-managed-code-evidence-and-provider-boundary.md) | Partially superseded | Seven static managed tools remain; runtime planning removed |
|
||||
|
||||
@@ -24,15 +24,19 @@ graph TD
|
||||
GHIDRA["src/ghidra/<br/>Ghidra discovery, profile & function analysis<br/>snapshot, Unix/Windows transport, bounded lifecycle"]
|
||||
AUX["src/artifacts/ + src/native/<br/>disjoint artifact and native capabilities"]
|
||||
BROWSER["src/browser/<br/>Loopback CDP transport<br/>exact-origin passive observation"]
|
||||
REPLAY["src/replay/<br/>Disposable JavaScript worker<br/>Bubblewrap, seccomp, cgroup limits"]
|
||||
DOTNET["src/dotnet/<br/>Managed static metadata provider<br/>PE/CLI metadata inventory, members, IL"]
|
||||
REFERENCE["src/reference/<br/>Filesystem reference-source reader<br/>bounded traversal, validation, errors"]
|
||||
end
|
||||
|
||||
subgraph "Provider Process Foundation"
|
||||
PROCESS["src/process/<br/>owned process groups, private roots<br/>deadlines, bounded file reads and diagnostics, cleanup"]
|
||||
WINDOWS["src/windows/<br/>Windows host boundary adapters<br/>package loader, opaque handles, owned jobs and private runtimes"]
|
||||
WINDOWS_NATIVE["native/windows/src/<br/>Node-API implementation<br/>NTFS admission, protected DACLs and atomic Job Object assignment"]
|
||||
end
|
||||
|
||||
subgraph "Contracts Layer"
|
||||
CONTRACTS["src/contracts/<br/>Caller-visible schemas for analysis, managed, browser, Electron, application, and session tools"]
|
||||
ERRORS["src/contracts/errorSchemas.ts<br/>Shared analysis error schemas<br/>CLI diagnostics, generated error catalog"]
|
||||
end
|
||||
|
||||
subgraph "Domain Layer"
|
||||
@@ -40,19 +44,24 @@ graph TD
|
||||
end
|
||||
|
||||
subgraph "Configuration"
|
||||
CONFIG["src/config.ts<br/>Zod-validated env parsing<br/>provider settings, origins, and approved roots"]
|
||||
CONFIG["src/config.ts<br/>Zod-validated env parsing<br/>provider settings and origins"]
|
||||
end
|
||||
|
||||
subgraph "Tests"
|
||||
TESTS["tests/<br/>Vitest suite<br/>fixtures/ holds fake launcher, bridge, and CDP seams"]
|
||||
end
|
||||
|
||||
subgraph "Evaluation"
|
||||
EVALUATION["src/evaluation/<br/>Agent release evaluation<br/>session transcript scoring"]
|
||||
end
|
||||
|
||||
DISPATCH --> CLI
|
||||
DISPATCH --> MAIN
|
||||
MAIN --> CONFIG
|
||||
MAIN --> SERVER
|
||||
MAIN --> APP
|
||||
CLI --> APP
|
||||
CLI --> ERRORS
|
||||
APP --> SESSION
|
||||
SESSION --> REGISTRY
|
||||
REGISTRY --> HOPPER
|
||||
@@ -63,25 +72,31 @@ graph TD
|
||||
SERVER --> CONTRACTS
|
||||
SERVER --> APP
|
||||
APP --> BROWSER
|
||||
APP --> REPLAY
|
||||
APP --> DOTNET
|
||||
DOTNET --> DOMAIN
|
||||
HOPPER --> HOPPER_BRIDGE
|
||||
GHIDRA --> GHIDRA_BRIDGE
|
||||
HOPPER --> PROCESS
|
||||
GHIDRA --> PROCESS
|
||||
GHIDRA --> WINDOWS
|
||||
PROCESS --> WINDOWS
|
||||
WINDOWS --> WINDOWS_NATIVE
|
||||
APP --> PROCESS
|
||||
APP --> DOMAIN
|
||||
JS_RECON --> AUX
|
||||
JS_RECON --> DOMAIN
|
||||
APP --> REFERENCE
|
||||
REFERENCE --> DOMAIN
|
||||
CONTRACTS --> DOMAIN
|
||||
HOPPER --> DOMAIN
|
||||
GHIDRA --> DOMAIN
|
||||
AUX --> DOMAIN
|
||||
BROWSER --> DOMAIN
|
||||
REPLAY --> DOMAIN
|
||||
PROCESS --> DOMAIN
|
||||
TESTS -.-> MAIN
|
||||
TESTS -.-> HOPPER
|
||||
TESTS -.-> GHIDRA
|
||||
TESTS -.-> PROCESS
|
||||
TESTS -.-> BROWSER
|
||||
TESTS -.-> REPLAY
|
||||
TESTS -.-> EVALUATION
|
||||
TESTS -.-> ERRORS
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Discord</title>
|
||||
<path fill="#5865F2" d="M20.317 4.3698a19.7913 19.7913 0 0 0-4.8851-1.5152.0741.0741 0 0 0-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 0 0-.0785-.037 19.7363 19.7363 0 0 0-4.8852 1.515.0699.0699 0 0 0-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 0 0 .0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 0 0 .0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 0 0-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 0 1-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 0 1 .0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 0 1 .0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 0 1-.0066.1276 12.2986 12.2986 0 0 1-1.873.8914.0766.0766 0 0 0-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 0 0 .0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 0 0 .0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 0 0-.0312-.0286ZM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189Zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
+28
-44
@@ -4,20 +4,20 @@ REA can attach to a user-owned Chrome-family browser through the Chrome DevTools
|
||||
|
||||
## Shipped surfaces
|
||||
|
||||
- `list_browser_targets` / `rea list-browser-targets` discovers page targets whose current URL matches an approved exact origin.
|
||||
- `inspect_web_page` / `rea inspect-web-page` captures DOM structure, accessibility nodes, scripts, resources, safe response/DOM metadata, attach-window network and console metadata, WebSocket frame sizes, workers, quota, and optionally storage key names, redacted storage-content fingerprints, or script sources.
|
||||
- `analyze_web_bundle` / `rea analyze-web-bundle` parses approved script artifacts without execution and derives chunk edges, route and endpoint candidates, vendor fingerprints, static WebMCP declarations, and optionally approved source-map/original-source evidence.
|
||||
- `list_browser_targets` / `rea list-browser-targets` discovers page targets at the selected target's current origin by default, with an optional exact-origin filter.
|
||||
- `inspect_web_page` / `rea inspect-web-page` captures DOM structure, accessibility nodes, scripts, resources, safe response/DOM metadata, attach-window network and console metadata, WebSocket frame sizes, workers, quota, and optionally selected storage key names, redacted storage-content fingerprints, or script sources.
|
||||
- `analyze_web_bundle` / `rea analyze-web-bundle` parses selected script artifacts without execution and derives chunk edges, route and endpoint candidates, vendor fingerprints, static WebMCP declarations, and optionally source-map/original-source evidence.
|
||||
- `observe_web_session` / `rea observe-web-session` arms an observation window of the requested duration for an external user action and records ordered reload, SPA navigation, redirect, failure, lifecycle, and target-termination metadata.
|
||||
- `discover_webmcp_tools` / `rea discover-webmcp-tools` uses the experimental CDP WebMCP domain to return every in-scope registration and its complete structural input-schema summary inline. REA never exposes `WebMCP.invokeTool`; malformed protocol payloads fail explicitly instead of returning incomplete tool metadata.
|
||||
- `compare_web_captures` / `rea compare-web-captures` compares passive captures or exact-step-aligned browser scenarios. Scenario results commit reproducible literal normalization and expose alignment failures plus artifact-level action, screenshot, DOM, accessibility, URL, history, storage, and event differences. Missing or truncated evidence is never treated as equivalence.
|
||||
- `capture_web_screenshot` / `rea capture-web-screenshot` returns an explicitly approved, content-addressed visible-viewport PNG.
|
||||
- `capture_web_screenshot` / `rea capture-web-screenshot` returns a content-addressed visible-viewport PNG.
|
||||
- `compare_web_screenshots` / `rea compare-web-screenshots` performs bounded local PNG pixel comparison without OCR or external services.
|
||||
- Every surface has equivalent CLI and MCP contracts and returns Evidence provenance.
|
||||
- MCP tools return complete results inline and include the Evidence ID. Session evidence can be exported with `export_evidence_bundle`.
|
||||
|
||||
Target discovery returns the complete approved target array in one result.
|
||||
Target discovery returns the complete in-scope target array in one result.
|
||||
|
||||
Electron `file://` pages use a separate provider, permission capability, and root model; see [electron-observation.md](electron-observation.md).
|
||||
Electron `file://` pages use a separate provider and target boundary; see [electron-observation.md](electron-observation.md).
|
||||
Existing static application Evidence and passive web/Electron captures can be
|
||||
combined later through
|
||||
[JavaScript static/runtime reconciliation](javascript-runtime-reconciliation.md).
|
||||
@@ -35,35 +35,20 @@ google-chrome \
|
||||
|
||||
REA does not launch, own, or terminate this browser. Use a dedicated profile and stop it yourself when the investigation is complete. Do not expose the debugging port on a non-loopback interface.
|
||||
|
||||
## Configure authority
|
||||
## Request boundary
|
||||
|
||||
Browser observation is fail-closed by default. Configure the administrator ceiling before starting REA:
|
||||
|
||||
```bash
|
||||
export REA_BROWSER_OBSERVE_ENABLED=true
|
||||
export REA_BROWSER_CDP_ENDPOINTS_JSON='["http://127.0.0.1:9222"]'
|
||||
export REA_BROWSER_ALLOWED_ORIGINS_JSON='["http://127.0.0.1:3000"]'
|
||||
```
|
||||
|
||||
`REA_BROWSER_CDP_ENDPOINTS_JSON` accepts at most 16 literal `http://127.0.0.1:PORT` or `http://[::1]:PORT` endpoints. `localhost`, private-LAN addresses, HTTPS endpoints, credentials, paths, queries, fragments, and implicit ports are rejected.
|
||||
|
||||
`REA_BROWSER_ALLOWED_ORIGINS_JSON` accepts at most 32 exact HTTP(S) origins. Paths, credentials, queries, fragments, and wildcards are rejected. An input must stay within both this administrator ceiling and any active project/session/once grant.
|
||||
|
||||
Configuration is reloadable through the existing `SIGHUP` permission-policy path. Each request is checked against the configured endpoint, origin, and permission grants.
|
||||
|
||||
Inspect the exact effective scope without contacting the browser:
|
||||
|
||||
```bash
|
||||
rea policy explain browser_observe \
|
||||
--origins http://127.0.0.1:9222 \
|
||||
--origins http://127.0.0.1:3000 \
|
||||
--network loopback \
|
||||
--json
|
||||
```
|
||||
Each request supplies a literal loopback CDP endpoint and target. Target
|
||||
discovery uses the selected target's current origin by default; an optional
|
||||
exact HTTP(S) origin filter can narrow the result. `localhost`, private-LAN
|
||||
addresses, HTTPS CDP endpoints, credentials, paths, queries, fragments, and
|
||||
implicit ports are not valid CDP endpoints. Origin filters contain only scheme,
|
||||
host, and port; paths, credentials, queries, fragments, and wildcards are not
|
||||
valid origins.
|
||||
|
||||
## CLI workflow
|
||||
|
||||
Configured origins are used by default. `--allowed-origins` can request a narrower set that still fits the configured ceiling.
|
||||
Pass the endpoint and selected target for each call. An origin filter is
|
||||
optional and narrows the target set.
|
||||
|
||||
```bash
|
||||
rea list-browser-targets http://127.0.0.1:9222 --json
|
||||
@@ -96,7 +81,7 @@ rea inspect-web-page http://127.0.0.1:9222 TARGET_ID \
|
||||
--json
|
||||
```
|
||||
|
||||
Accessibility text and script content may contain application secrets and become part of the returned Evidence. Credential-shaped console substrings are redacted. JSON and WebSocket captures retain complete paths, types, and counts, never values or examples. Storage values remain redacted even when key-name capture is requested. `--include-storage-fingerprints` requests SHA-256 identity/value fingerprints, which support comparison only when cookies, DOM storage, IndexedDB, and Cache Storage were all captured completely.
|
||||
Accessibility text, script content, query values, and explicitly requested primitive console text may contain application secrets and become part of the returned Evidence. URL username/password credentials are removed; ordinary query values, fragments, and parameter order remain intact. JSON and WebSocket captures retain complete paths, types, and counts, never values or examples. Storage values remain redacted even when key-name capture is requested. `--include-storage-fingerprints` requests SHA-256 identity/value fingerprints, which support comparison only when cookies, DOM storage, IndexedDB, and Cache Storage were all captured completely.
|
||||
|
||||
Screenshot capture returns the complete visible viewport as an inline image artifact:
|
||||
|
||||
@@ -110,7 +95,6 @@ rea capture-web-screenshot http://127.0.0.1:9222 TARGET_ID \
|
||||
```json
|
||||
{
|
||||
"cdp_endpoint": "http://127.0.0.1:9222",
|
||||
"allowed_origins": ["http://127.0.0.1:3000"],
|
||||
"target_id": "TARGET_ID_FROM_LIST_BROWSER_TARGETS"
|
||||
}
|
||||
```
|
||||
@@ -121,25 +105,25 @@ Collection counts are not capped. Per-value validation, the selected observation
|
||||
window, and CDP protocol limits still apply; any incomplete or truncated values
|
||||
are reported in the result.
|
||||
|
||||
Call `list_browser_targets` first because target IDs are browser-instance-specific. REA rechecks the selected target's current type and origin immediately before attaching.
|
||||
Call `list_browser_targets` first because target IDs are browser-instance-specific. REA rechecks the selected target's current type and origin immediately before attaching. Without an origin filter, discovery lists all eligible pages; an individual capture defaults to the selected page's current origin.
|
||||
|
||||
REA establishes an authorized main-frame boundary before enabling Runtime, Debugger, or Network observation, rechecks it before document capture, and validates it again afterward. If the main frame navigates during final capture, REA discards the mixed result and returns `target_changed`.
|
||||
REA establishes a main-frame origin boundary before enabling Runtime, Debugger, or Network observation, rechecks it before document capture, and validates it again afterward. If the main frame navigates during final capture, REA discards the mixed result and returns `target_changed`.
|
||||
|
||||
## Data minimization
|
||||
|
||||
REA removes sensitive values before normalized event data is retained:
|
||||
REA retains local observation data and removes only URL userinfo credentials, explicitly declared scenario secrets, and values from structured credential/storage fields:
|
||||
|
||||
- URLs retain a bounded path and at most 256 bounded query parameter names, replace every retained query value with `[REDACTED]`, and remove credentials and fragments.
|
||||
- URL observations preserve the complete local URL, including query values, fragments, duplicate parameters, and their order. Only username/password userinfo is removed; malformed original URL strings remain available with `origin: null`.
|
||||
- DOM snapshots retain node types, node names, value lengths, and attribute names, but not text or attribute values.
|
||||
- Accessibility structure and roles are retained by default, while names and descriptions require `include_accessibility_text: true`.
|
||||
- Network observations retain method, status, MIME type, size, type, initiator stack location, and redacted URLs. Headers are discarded after an allowlisted projection of length/encoding, structured CSP/Link/policy fields, and untrusted agent hints. Cookies and authorization headers are never retained.
|
||||
- Network observations retain method, status, MIME type, size, type, initiator stack location, and complete URLs with userinfo credentials removed. Headers are discarded after an allowlisted projection of length/encoding, structured CSP/Link/policy fields, and untrusted agent hints. Cookies and authorization headers are never retained.
|
||||
- Request/response bodies are not requested or parsed by default. When selected, only allowed-origin JSON media types are read, converted immediately to complete value-free property paths/types, and discarded. `Network.getResponseBody` is never sent unless JSON body shape capture is selected.
|
||||
- Console observations with a stack source retain call type, argument types, timestamp, and redacted source location. When console text capture is selected, already-delivered primitive values are retained after credential redaction; objects, getters, and remote properties are never expanded.
|
||||
- Console observations with a stack source retain call type, argument types, timestamp, and source location. When console text capture is selected, already-delivered primitive values are retained verbatim; objects, getters, and remote properties are never expanded.
|
||||
- WebSocket observations retain direction, opcode, and payload byte length. When shape capture is selected, text frames are classified as text or complete value-free JSON shape; binary bytes, hashes, prefixes, and raw frames are never retained.
|
||||
- Storage observations always redact values. Key names, IndexedDB names, and cache names require `include_storage_keys`; stable content fingerprints require the additional `include_storage_fingerprints` selection. Cache bodies above 64 KiB and partial IndexedDB remote objects make fingerprint coverage incomplete, so identical observations remain `unknown`.
|
||||
- Script metadata is included only when CDP supplies a URL on an allowed origin. Stable keys exclude transient CDP script IDs, and exact transient raw URLs are used only during script/resource reconciliation. URL-less scripts are excluded because their origin cannot be established. When CDP supplies an execution-context association, the accepted script retains its authorized frame ID for later attribution. Source content is omitted unless explicitly requested and is returned inline as a self-verifying artifact with its SHA-256 digest.
|
||||
|
||||
Cross-origin frames, resources, scripts, events, and workers are excluded unless their exact origins are also approved. Excluded target details are counted without being exposed.
|
||||
Frames, resources, scripts, events, and workers outside the selected origin scope are excluded. An explicit `allowed_origins` list can include additional origins; excluded target details are counted without being exposed.
|
||||
Retained workers include validated opener-target and parent-frame IDs when CDP
|
||||
provides them; these relationships do not expand the exact-origin boundary or
|
||||
prove static module ownership.
|
||||
@@ -156,12 +140,12 @@ coverage. It reports excluded items and any values omitted or truncated by
|
||||
validation; it does not drop records to meet an internal collection quota.
|
||||
Disallowed-origin entries are filtered before retention.
|
||||
|
||||
CDP discovery, WebSocket connection, and each command have a 5-second timeout. Version discovery is capped at 64 KiB, target discovery at 2 MiB, each WebSocket message at 16 MiB, and correlated pending commands at 128. Target and navigation-event inventories have no separate count cap. These transport bounds protect protocol framing and request correlation; they do not silently trim returned findings. Malformed unsolicited protocol events poison the connection so later commands fail closed.
|
||||
CDP discovery, WebSocket connection, and commands continue until the peer responds or the caller cancels the operation; REA has no internal byte, message-count, or pending-command quota for these exchanges. Without caller cancellation, a stalled peer can leave an operation waiting. On cancellation, REA closes only its own WebSocket, waiting up to one second before terminating it. Malformed unsolicited protocol events poison the connection so later commands fail closed.
|
||||
|
||||
Network and console coverage starts only after REA attaches and enables the relevant CDP domains. `prior_activity_available` is always `false`; absence from these arrays is not evidence that an event never occurred. Source maps are not fetched by inspection. Bundle analysis can fetch them only after separate approval, with exact-origin redirect checks and no credentials, cookies, or referrer. Every requested map, mapping, original source, AST node, and finding from parsed content is returned inline; failed maps remain explicitly unavailable and make bundle completeness partial.
|
||||
Network and console coverage starts only after REA attaches and enables the relevant CDP domains. `prior_activity_available` is always `false`; absence from these arrays is not evidence that an event never occurred. Source maps are not fetched by inspection. Bundle analysis fetches them only when `fetch_source_maps` is selected, with exact-origin redirect checks and no credentials, cookies, or referrer. Every requested map, mapping, original source, AST node, and finding from parsed content is returned inline; failed maps remain explicitly unavailable and make bundle completeness partial.
|
||||
|
||||
## Non-goals and threat model
|
||||
|
||||
The browser provider deliberately does not expose generic CDP commands, `Runtime.evaluate`, WebMCP invocation, navigation, input, downloads, page closure, or browser closure. Screenshot capture is the sole pixel surface and requires separate literal approval. REA disables the domains it enabled, detaches the target session, and closes only its own WebSocket.
|
||||
The browser provider deliberately does not expose generic CDP commands, `Runtime.evaluate`, WebMCP invocation, navigation, input, downloads, page closure, or browser closure. Screenshot capture is the sole pixel surface and returns an image only when requested. REA disables the domains it enabled, detaches the target session, and closes only its own WebSocket.
|
||||
|
||||
This feature is not a browser sandbox or network containment mechanism. The attached page and browser continue running with their existing privileges and may make external requests independently of REA. CDP gives deep access to the selected browser profile, so use a dedicated profile, approve only origins you intend to inspect, and treat other same-user processes as outside this boundary.
|
||||
This feature is not a browser sandbox or network containment mechanism. The attached page and browser continue running with their existing privileges and may make external requests independently of REA. CDP gives deep access to the selected browser profile, so use a dedicated profile, include only origins you intend to inspect, and treat other same-user processes as outside this boundary.
|
||||
|
||||
@@ -3,42 +3,49 @@
|
||||
`capture_browser_scenario` accepts the provider-neutral
|
||||
`browserScenarioSchema` through both MCP and the
|
||||
`capture-browser-scenario INPUT_JSON` CLI command. The request is declarative:
|
||||
it admits a fixed action vocabulary, exact HTTP(S) origins, deterministic
|
||||
browser settings, explicit storage and request replay, and provider-owned
|
||||
it admits a fixed action vocabulary, explicit HTTP(S) targets, deterministic
|
||||
browser settings, explicit storage seeds, and provider-owned
|
||||
liveness deadlines. `INPUT_JSON` may be inline JSON or a JSON file path.
|
||||
|
||||
The minimal request contains only browser launch/connect authority, `start_url`,
|
||||
`allowed_origins`, and at least one `actions` entry. Environment settings,
|
||||
empty storage, disabled request replay, the required redaction policy, and a
|
||||
final sanitized URL capture are supplied by default. Add secret declarations,
|
||||
storage seeds, replay routes, query redaction names, and additional artifact or
|
||||
event capture only when the investigation needs them. Action, secret, storage,
|
||||
and replay-route counts are not capped. Duration, action, and navigation
|
||||
The minimal request contains only browser launch/connect selection, `start_url`,
|
||||
and at least one `actions` entry. Launch `headless` defaults to `true` and can
|
||||
be set to `false` where the host supports a visible browser. Navigations follow
|
||||
the destinations named by the scenario and the page's ordinary browser requests;
|
||||
there is no separate origin allowlist or request-interception layer. Environment
|
||||
settings (including service workers blocked by default), empty storage, and a
|
||||
final sanitized URL capture are supplied by default. Set
|
||||
`environment.service_workers` to `allow` when the application requires them.
|
||||
Add secret declarations, storage seeds, and additional artifact or event
|
||||
capture only when needed. Action, secret, and storage counts are not capped.
|
||||
Duration, action, and navigation
|
||||
timeouts remain fixed provider-owned liveness limits; the request does not
|
||||
accept a caller-controlled `limits` object.
|
||||
|
||||
The browser boundary is part of the contract. Launch mode requires a
|
||||
caller-selected absolute executable, a provider-owned temporary profile,
|
||||
headless operation, and close-plus-delete cleanup. Connect mode accepts only an
|
||||
explicit-port loopback CDP endpoint, declares the browser externally owned, and
|
||||
permits disconnect-only cleanup. The Playwright driver preserves this
|
||||
caller-selected executable and always uses a provider-owned temporary profile
|
||||
that is closed and deleted during cleanup. Connect mode accepts only an
|
||||
explicit-port loopback CDP endpoint. The Playwright driver preserves this
|
||||
distinction: it closes and deletes provider-owned profiles, but only disconnects
|
||||
from an external CDP browser. Real-browser verification checks both outcomes and
|
||||
confirms that an attached external browser remains alive.
|
||||
|
||||
URLs contain a query-free HTTP(S) base plus ordered query entries. Every query,
|
||||
form, storage, cookie, and replay value is either an explicitly public literal
|
||||
or a reference to a declared environment-backed secret. Raw credentials in
|
||||
URLs and literal credential headers are rejected. Secret query names must also
|
||||
appear in the redaction policy, and the required credential headers cannot be
|
||||
removed from that policy. Durable results must replace resolved secret values
|
||||
HTTP(S) URLs may include ordinary query values and fragments directly. Use
|
||||
ordered structured query entries when a value needs to reference a declared
|
||||
environment-backed secret. Form, storage, and cookie values are either literal
|
||||
strings or declared secret references. Raw URL userinfo credentials are rejected.
|
||||
Captured credential-header
|
||||
values are not retained, and declared secret values are redacted automatically.
|
||||
A secret may be declared solely to
|
||||
redact matching observed content; every secret reference in an action, URL,
|
||||
storage value still needs a declaration. Ordinary query values
|
||||
and fragments remain intact. Durable results replace resolved secret values
|
||||
with their secret references.
|
||||
|
||||
Every start, navigation, storage, replay, and redirect origin must be listed in
|
||||
`allowed_origins`. Unsupported action tags, unknown fields, duplicate step or
|
||||
route IDs, undeclared or unused secrets, and provider-owned replay headers fail
|
||||
validation. HTTP and WebSocket routing enforces the same declared scope. Exact
|
||||
replay may abort unmatched requests or pass through only approved origins.
|
||||
The exact `start_url`, navigation destinations, and storage-seed origins define
|
||||
the inputs to a scenario; ordinary page requests are handled by the browser.
|
||||
Storage cookies are scoped to the page URL at each capture. Unsupported action
|
||||
tags, unknown fields, duplicate step IDs, and undeclared secret references fail
|
||||
validation.
|
||||
|
||||
The result is Evidence with an initial state followed by one record per
|
||||
declared action. Each step reports action status, elapsed time, sanitized URLs,
|
||||
@@ -53,21 +60,30 @@ inline without application-defined size ceilings. Other missing sections
|
||||
remain explicit and make the capture ineligible for equality claims.
|
||||
Attach-mode captures also declare the unavoidable pre-attach event gap.
|
||||
|
||||
Browser automation is disabled by default. The administrator ceiling is
|
||||
configured with:
|
||||
Selected event families are observed on the root page and discovered descendant
|
||||
popups, including nested popups. Popup lifecycle records are emitted only when
|
||||
`popups` is selected. Launch-mode network observation starts at the owned
|
||||
context before navigation, so popup document requests, redirects, and responses
|
||||
are included once. Connected contexts can contain unrelated tabs: only network
|
||||
events with a frame belonging to the root or a discovered popup are retained.
|
||||
Initial popup requests without a frame cannot be attributed in shared contexts
|
||||
and are omitted; attach coverage remains incomplete.
|
||||
|
||||
- `REA_BROWSER_SCENARIO_ENABLED`
|
||||
- `REA_BROWSER_SCENARIO_EXECUTABLE_ROOTS_JSON`
|
||||
- `REA_BROWSER_SCENARIO_CDP_ENDPOINTS_JSON`
|
||||
- `REA_BROWSER_SCENARIO_ALLOWED_ORIGINS_JSON`
|
||||
- `REA_BROWSER_SCENARIO_ALLOWED_ENV_JSON`
|
||||
Playwright reports new popup Pages after their first navigation has committed.
|
||||
It buffers early console messages and page errors, but the pinned API cannot
|
||||
recover every earlier frame, worker, WebSocket, or download event. If one of
|
||||
those families is selected and a popup is discovered, REA reports the family gap
|
||||
in `limitations`, marks `events` missing, and disables equality eligibility.
|
||||
Later events remain available; no initial navigation is synthesized from the
|
||||
popup's current URL.
|
||||
|
||||
Unlike passive observation, browser automation is not granted automatically.
|
||||
Use an explicit project/session grant, or opt into the administrator grant with
|
||||
`REA_BROWSER_SCENARIO_AUTO_GRANT=true` for a trusted unattended environment.
|
||||
Authorization commits the exact executable or CDP endpoint, origins, environment
|
||||
variable names, network class, and scenario digest before any browser side
|
||||
effect.
|
||||
Each request names the exact launch executable or loopback CDP endpoint, target,
|
||||
actions, and capture behavior. Origin filters and environment-variable names
|
||||
are included when the scenario needs them.
|
||||
Launch owns a temporary profile; connect mode disconnects from the selected
|
||||
external browser without closing it. The request itself defines the operation;
|
||||
there is no additional REA grant step. The browser and host still enforce their
|
||||
own access and process rules.
|
||||
|
||||
## Scenario comparison
|
||||
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
# Controlled JavaScript replay
|
||||
|
||||
`run_controlled_replay` executes only operator-selected extracted modules. It
|
||||
does not launch or drive the original application. Static inference, passive
|
||||
browser/Electron observation, isolated replay, and real application behavior
|
||||
remain different authorities.
|
||||
|
||||
## Enable the ceiling
|
||||
|
||||
Replay is disabled by default. Configure exact source roots before REA starts:
|
||||
|
||||
```sh
|
||||
export REA_JAVASCRIPT_REPLAY_ENABLED=true
|
||||
export REA_JAVASCRIPT_REPLAY_ROOTS_JSON='["/absolute/extracted/modules"]'
|
||||
```
|
||||
|
||||
The default trusted executables are the current Node.js runtime,
|
||||
`/usr/bin/bwrap`, `/usr/bin/systemd-run`, `/usr/bin/systemctl`, and
|
||||
`/usr/bin/bash`. Override them only with the corresponding
|
||||
`REA_JAVASCRIPT_REPLAY_*_PATH` variable. Configuration creates the
|
||||
administrator ceiling; it does not replace per-call approval. `rea doctor`
|
||||
reports disabled, available, and exact sandbox-probe failures without running
|
||||
module code. REA setup never installs these host components.
|
||||
|
||||
## Plan, review, execute
|
||||
|
||||
Call the same CLI command or MCP tool twice. The first request uses
|
||||
`mode: "plan"`. Its response commits module paths and digests, closed dependency
|
||||
aliases, cases, deterministic providers, runtime/backend identities, resource
|
||||
limits, no-network policy, private filesystems, and a `plan_digest`.
|
||||
Runtime commitments include the exact worker, seccomp filter, Node executable,
|
||||
ELF loader, and shared-library source/destination paths and SHA-256 digests.
|
||||
|
||||
The execute request must repeat the same manifest and include:
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "execute",
|
||||
"approved": true,
|
||||
"plan_digest": "<exact digest returned by plan>"
|
||||
}
|
||||
```
|
||||
|
||||
REA rebuilds the plan. Any changed module, stub, case, runtime, backend, limit,
|
||||
or export commitment returns `plan_stale` before worker admission.
|
||||
|
||||
Modules use `esm` or `commonjs-factory`. The latter accepts extracted Rspack
|
||||
factory syntax such as `123(module, exports, require) { ... }` and implements
|
||||
the bounded `require.d`, `require.r`, `require.n`, and `require.nmd` helpers.
|
||||
Every import or require must map to a declared alias. There is no ambient
|
||||
package resolution.
|
||||
|
||||
Explicit cases can be combined with deterministic `parser-boundaries`,
|
||||
`sanitizer-boundaries`, or `clipboard-boundaries` generation. Supplying a
|
||||
`right` manifest runs both sides with fresh realms per case and reports
|
||||
`equal`, `changed`, or `unknown` comparisons.
|
||||
|
||||
## Boundary and Evidence
|
||||
|
||||
The worker receives module bytes over bounded stdin and runs under a fresh
|
||||
Bubblewrap user/PID/network/IPC/UTS namespace, an architecture-checked seccomp
|
||||
filter, an empty mount root with a descriptor-backed read-only Node closure,
|
||||
private tmpfs, and a transient systemd user cgroup. Host and external network,
|
||||
host writes, process spawning, workers, native addons, inspector access, and
|
||||
ambient environment are unavailable. Wall time, memory, swap, tasks, CPU,
|
||||
case input, worker-protocol input, output, stderr, depth, and node counts are
|
||||
independently bounded. The parent also requires an exact worker response shape
|
||||
and authenticates every returned case ID, order, and input digest. Values are
|
||||
projected through own data descriptors only; Proxy values and accessors are
|
||||
rejected without invoking their traps or getters.
|
||||
|
||||
Returns, exceptions, serialization failures, denials, timeouts, OOMs, crashes,
|
||||
protocol failures, and cleanup state are observations with provider
|
||||
`rea-javascript-replay` and authority `controlled-replay`. An observation does
|
||||
not prove that the original renderer, preload, main process, browser, or remote
|
||||
service behaved identically.
|
||||
|
||||
Each left/right run is retained as its own `observed` source Evidence. A
|
||||
differential envelope is `derived` and links those source Evidence IDs, so the
|
||||
comparison never erases the underlying observations.
|
||||
|
||||
Optional `reproducer_export` is committed by the plan and writes to its
|
||||
caller-supplied path after the sandbox has stopped.
|
||||
The owner-only manifest is written only after complete sandbox cleanup. Source
|
||||
bytes are excluded unless `include_sources: true` was explicitly approved.
|
||||
An export failure is retained in the result and does not erase a completed
|
||||
replay observation.
|
||||
|
||||
## Local real-artifact verification
|
||||
|
||||
`npm run verify:replay` uses source-owned parser and hostile fixtures. An
|
||||
operator can additionally supply a local ESM or Rspack/CommonJS-factory
|
||||
manifest without copying module source into the repository:
|
||||
|
||||
```sh
|
||||
REA_REPLAY_INPUT_PATH=/absolute/replay-manifest.json npm run verify:replay
|
||||
```
|
||||
|
||||
The verifier canonicalizes the manifest's module paths and prints only plan,
|
||||
module, and Evidence digests, case/comparison summaries, and cleanup state.
|
||||
This is the intended seam for local Notion-like parser/sanitizer benchmarks.
|
||||
@@ -1,24 +1,23 @@
|
||||
# Electron file-page observation
|
||||
|
||||
REA can attach to a user-owned Electron/Chromium CDP endpoint and inspect existing `file://` renderer pages without evaluating JavaScript or invoking Electron APIs. The caller supplies the endpoint directly; REA accepts only literal-port loopback HTTP endpoints. Selecting an endpoint exposes every Electron target it serves, including local file paths and page metadata.
|
||||
REA can attach to an Electron/Chromium CDP endpoint and inspect existing `file://` renderer pages without evaluating JavaScript or invoking Electron APIs. The request supplies the endpoint directly; REA accepts only literal-port loopback HTTP endpoints. Selecting an endpoint exposes every Electron target it serves, including local file paths and page metadata.
|
||||
|
||||
This passive runtime surface is distinct from the target-free static
|
||||
[`analyze_javascript_application`](javascript-artifact-reconstruction.md)
|
||||
workflow. Static analysis reads an approved ASAR or extracted directory under
|
||||
observations and static inferences are never silently treated as the same fact.
|
||||
workflow. Static analysis reads a local ASAR or extracted directory without
|
||||
executing application code. Passive observation inspects an existing Electron
|
||||
page. The results keep static inferences and runtime observations distinct.
|
||||
Use the separate
|
||||
[`reconcile_javascript_runtime`](javascript-runtime-reconciliation.md) workflow
|
||||
when both Evidence sets already exist.
|
||||
|
||||
## Configure authority
|
||||
## Target boundary
|
||||
|
||||
The capability is disabled by default:
|
||||
|
||||
```bash
|
||||
export REA_ELECTRON_OBSERVE_ENABLED=true
|
||||
```
|
||||
|
||||
The capability is disabled by default. Its permission ceiling is loopback-only. REA accepts local hostless `file://` URLs that resolve to regular files; remote hosts, encoded path separators, and nonexistent paths are rejected. There is no separate filesystem-root configuration.
|
||||
The request supplies a literal-port loopback endpoint and, for inspection, a
|
||||
target ID. REA accepts local hostless `file://` URLs that resolve to regular
|
||||
files; remote hosts, encoded path separators, and nonexistent paths are
|
||||
rejected. The endpoint exposes every eligible target and its local path
|
||||
metadata.
|
||||
|
||||
## Workflow
|
||||
|
||||
@@ -32,7 +31,8 @@ rea inspect-electron-page http://127.0.0.1:9223 TARGET_ID \
|
||||
--observation-ms 100 --json
|
||||
```
|
||||
|
||||
Script content is excluded by default. Requesting it includes every authorized script source:
|
||||
Script content is excluded by default. Setting `include_script_sources` in the
|
||||
request returns the selected target's script sources:
|
||||
|
||||
```bash
|
||||
rea inspect-electron-page http://127.0.0.1:9223 TARGET_ID \
|
||||
@@ -42,9 +42,9 @@ rea inspect-electron-page http://127.0.0.1:9223 TARGET_ID \
|
||||
|
||||
The normalized result contains canonical local paths, frame and DOM
|
||||
structure, resource metadata, stable script/resource identities, explicit
|
||||
completeness, and content-addressed approved source artifacts. Script metadata
|
||||
completeness, and content-addressed script artifacts. Script metadata
|
||||
retains its execution-context frame ID when CDP supplies one. The capture also
|
||||
inventories authorized worker, service-worker, and shared-worker targets with
|
||||
inventories worker, service-worker, and shared-worker targets with
|
||||
validated opener-target and parent-frame IDs. Worker discovery uses passive
|
||||
target metadata; REA does not attach to or execute code in those targets.
|
||||
Collection counts and aggregate script-source bytes are not capped. Like every
|
||||
@@ -58,31 +58,25 @@ invoke Electron IPC, close a target, or terminate the application.
|
||||
|
||||
## Active Electron scenarios
|
||||
|
||||
Active Electron authority is separate and disabled by default. When enabled,
|
||||
REA launches an operator-approved Electron executable through the official
|
||||
Playwright Electron API, owns its lifetime, accepts bounded click/wait actions,
|
||||
The scenario request supplies the Electron executable and application entry
|
||||
point, plus any arguments and actions. The application root defaults to the
|
||||
entry point's parent directory and can be supplied when the app uses a different
|
||||
root. REA launches that executable through
|
||||
the official Playwright Electron API and owns its lifetime. It accepts click/wait actions,
|
||||
window-targeted renderer reload/crash actions, and synthetic `open-url` or
|
||||
`second-instance` deep-link delivery. It records capture-scoped window and
|
||||
WebContents identities, process metrics, and IPC channel and value-shape metadata.
|
||||
Payload values are not retained.
|
||||
|
||||
Configure exact roots and run the real fixture verifier with an operator-owned
|
||||
Electron runtime:
|
||||
Run the real fixture verifier with an operator-provided Electron runtime:
|
||||
|
||||
```bash
|
||||
export REA_ELECTRON_AUTOMATE_ENABLED=true
|
||||
export REA_ELECTRON_AUTOMATE_AUTO_GRANT=false
|
||||
export REA_ELECTRON_AUTOMATE_EXECUTABLE_ROOTS_JSON='["/absolute/path/to/runtime"]'
|
||||
export REA_ELECTRON_AUTOMATE_APPLICATION_ROOTS_JSON='["/absolute/path/to/app"]'
|
||||
REA_ELECTRON_EXECUTABLE=/absolute/path/to/electron npm run verify:electron
|
||||
```
|
||||
|
||||
This capability actively launches and interacts with the target. It is not a
|
||||
passive CDP observation and must be granted separately as `electron_automate`.
|
||||
The default is fail-closed (`AUTO_GRANT=false`); an operator can instead issue
|
||||
a project/session/one-shot grant through the normal permission workflow. The
|
||||
owned process keeps normal host filesystem and network privileges, so this is
|
||||
an authority boundary and lifecycle boundary, not a sandbox.
|
||||
This operation actively launches and interacts with the selected target. It is
|
||||
not a passive CDP observation. The owned process keeps normal host filesystem
|
||||
and network privileges, so lifecycle ownership does not make it a sandbox.
|
||||
|
||||
The CLI and MCP surfaces accept the same schema. For a JSON request file:
|
||||
|
||||
@@ -105,7 +99,7 @@ caller-selected capture limits.
|
||||
|
||||
Active Electron Evidence can also be supplied to
|
||||
[`reconcile_javascript_runtime`](javascript-runtime-reconciliation.md). That
|
||||
projection is intentionally target-only and partial: it binds the approved
|
||||
projection is intentionally target-only and partial: it binds the selected
|
||||
application path and capture outcome to the static graph, while frames, scripts,
|
||||
workers, and execution claims remain unavailable until a separate passive runtime
|
||||
capture provides them.
|
||||
|
||||
+32
-282
@@ -9,7 +9,6 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"invalid_input",
|
||||
"permission_required",
|
||||
"unsupported_provider",
|
||||
"integrity_mismatch",
|
||||
"truncated",
|
||||
@@ -23,12 +22,8 @@
|
||||
"retryable": { "type": "boolean" },
|
||||
"remediation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": { "type": "string", "minLength": 1 },
|
||||
"restart_required": { "type": "boolean" },
|
||||
"elicitation_supported": { "type": "boolean" }
|
||||
},
|
||||
"required": ["action", "restart_required"],
|
||||
"properties": { "action": { "type": "string", "minLength": 1 } },
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"details": {
|
||||
@@ -48,7 +43,6 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"invalid_input",
|
||||
"permission_required",
|
||||
"unsupported_provider",
|
||||
"integrity_mismatch",
|
||||
"truncated",
|
||||
@@ -62,12 +56,8 @@
|
||||
"retryable": { "type": "boolean" },
|
||||
"remediation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": { "type": "string", "minLength": 1 },
|
||||
"restart_required": { "type": "boolean" },
|
||||
"elicitation_supported": { "type": "boolean" }
|
||||
},
|
||||
"required": ["action", "restart_required"],
|
||||
"properties": { "action": { "type": "string", "minLength": 1 } },
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"details": {
|
||||
@@ -87,7 +77,6 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"invalid_input",
|
||||
"permission_required",
|
||||
"unsupported_provider",
|
||||
"integrity_mismatch",
|
||||
"truncated",
|
||||
@@ -101,12 +90,8 @@
|
||||
"retryable": { "type": "boolean" },
|
||||
"remediation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": { "type": "string", "minLength": 1 },
|
||||
"restart_required": { "type": "boolean" },
|
||||
"elicitation_supported": { "type": "boolean" }
|
||||
},
|
||||
"required": ["action", "restart_required"],
|
||||
"properties": { "action": { "type": "string", "minLength": 1 } },
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"details": {
|
||||
@@ -126,7 +111,6 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"invalid_input",
|
||||
"permission_required",
|
||||
"unsupported_provider",
|
||||
"integrity_mismatch",
|
||||
"truncated",
|
||||
@@ -140,12 +124,8 @@
|
||||
"retryable": { "type": "boolean" },
|
||||
"remediation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": { "type": "string", "minLength": 1 },
|
||||
"restart_required": { "type": "boolean" },
|
||||
"elicitation_supported": { "type": "boolean" }
|
||||
},
|
||||
"required": ["action", "restart_required"],
|
||||
"properties": { "action": { "type": "string", "minLength": 1 } },
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"details": {
|
||||
@@ -165,7 +145,6 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"invalid_input",
|
||||
"permission_required",
|
||||
"unsupported_provider",
|
||||
"integrity_mismatch",
|
||||
"truncated",
|
||||
@@ -179,12 +158,8 @@
|
||||
"retryable": { "type": "boolean" },
|
||||
"remediation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": { "type": "string", "minLength": 1 },
|
||||
"restart_required": { "type": "boolean" },
|
||||
"elicitation_supported": { "type": "boolean" }
|
||||
},
|
||||
"required": ["action", "restart_required"],
|
||||
"properties": { "action": { "type": "string", "minLength": 1 } },
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"details": {
|
||||
@@ -204,7 +179,6 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"invalid_input",
|
||||
"permission_required",
|
||||
"unsupported_provider",
|
||||
"integrity_mismatch",
|
||||
"truncated",
|
||||
@@ -218,12 +192,8 @@
|
||||
"retryable": { "type": "boolean" },
|
||||
"remediation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": { "type": "string", "minLength": 1 },
|
||||
"restart_required": { "type": "boolean" },
|
||||
"elicitation_supported": { "type": "boolean" }
|
||||
},
|
||||
"required": ["action", "restart_required"],
|
||||
"properties": { "action": { "type": "string", "minLength": 1 } },
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"details": {
|
||||
@@ -243,7 +213,6 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"invalid_input",
|
||||
"permission_required",
|
||||
"unsupported_provider",
|
||||
"integrity_mismatch",
|
||||
"truncated",
|
||||
@@ -257,12 +226,8 @@
|
||||
"retryable": { "type": "boolean" },
|
||||
"remediation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": { "type": "string", "minLength": 1 },
|
||||
"restart_required": { "type": "boolean" },
|
||||
"elicitation_supported": { "type": "boolean" }
|
||||
},
|
||||
"required": ["action", "restart_required"],
|
||||
"properties": { "action": { "type": "string", "minLength": 1 } },
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"details": {
|
||||
@@ -304,7 +269,6 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"invalid_input",
|
||||
"permission_required",
|
||||
"unsupported_provider",
|
||||
"integrity_mismatch",
|
||||
"truncated",
|
||||
@@ -318,12 +282,8 @@
|
||||
"retryable": { "type": "boolean" },
|
||||
"remediation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": { "type": "string", "minLength": 1 },
|
||||
"restart_required": { "type": "boolean" },
|
||||
"elicitation_supported": { "type": "boolean" }
|
||||
},
|
||||
"required": ["action", "restart_required"],
|
||||
"properties": { "action": { "type": "string", "minLength": 1 } },
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"details": {
|
||||
@@ -343,7 +303,6 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"invalid_input",
|
||||
"permission_required",
|
||||
"unsupported_provider",
|
||||
"integrity_mismatch",
|
||||
"truncated",
|
||||
@@ -357,12 +316,8 @@
|
||||
"retryable": { "type": "boolean" },
|
||||
"remediation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": { "type": "string", "minLength": 1 },
|
||||
"restart_required": { "type": "boolean" },
|
||||
"elicitation_supported": { "type": "boolean" }
|
||||
},
|
||||
"required": ["action", "restart_required"],
|
||||
"properties": { "action": { "type": "string", "minLength": 1 } },
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"details": {
|
||||
@@ -382,7 +337,6 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"invalid_input",
|
||||
"permission_required",
|
||||
"unsupported_provider",
|
||||
"integrity_mismatch",
|
||||
"truncated",
|
||||
@@ -396,12 +350,8 @@
|
||||
"retryable": { "type": "boolean" },
|
||||
"remediation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": { "type": "string", "minLength": 1 },
|
||||
"restart_required": { "type": "boolean" },
|
||||
"elicitation_supported": { "type": "boolean" }
|
||||
},
|
||||
"required": ["action", "restart_required"],
|
||||
"properties": { "action": { "type": "string", "minLength": 1 } },
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"details": {
|
||||
@@ -413,137 +363,6 @@
|
||||
"required": ["code", "category", "message", "retryable", "remediation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": { "type": "string", "const": "permission_required" },
|
||||
"category": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"invalid_input",
|
||||
"permission_required",
|
||||
"unsupported_provider",
|
||||
"integrity_mismatch",
|
||||
"truncated",
|
||||
"cancelled",
|
||||
"timeout",
|
||||
"unavailable",
|
||||
"execution_failure"
|
||||
]
|
||||
},
|
||||
"message": { "type": "string", "minLength": 1 },
|
||||
"retryable": { "type": "boolean" },
|
||||
"remediation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": { "type": "string", "minLength": 1 },
|
||||
"restart_required": { "type": "boolean" },
|
||||
"elicitation_supported": { "type": "boolean" }
|
||||
},
|
||||
"required": ["action", "restart_required"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"capability": { "type": "string" },
|
||||
"requested": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"capability": { "type": "string" },
|
||||
"roots": { "type": "array", "items": { "type": "string" } },
|
||||
"executables": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"environment_names": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"origins": { "type": "array", "items": { "type": "string" } },
|
||||
"network": {
|
||||
"type": "string",
|
||||
"enum": ["none", "loopback", "external"]
|
||||
},
|
||||
"mount": { "type": "boolean" }
|
||||
},
|
||||
"required": [
|
||||
"capability",
|
||||
"roots",
|
||||
"executables",
|
||||
"environment_names",
|
||||
"network",
|
||||
"mount"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"missing": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"roots": { "type": "array", "items": { "type": "string" } },
|
||||
"executables": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"environment_names": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"origins": { "type": "array", "items": { "type": "string" } },
|
||||
"network": {
|
||||
"type": "string",
|
||||
"enum": ["none", "loopback", "external"]
|
||||
},
|
||||
"mount": { "type": "boolean", "const": true }
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"ceiling": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"capability": { "type": "string" },
|
||||
"roots": { "type": "array", "items": { "type": "string" } },
|
||||
"executables": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"environment_names": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"origins": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"network": {
|
||||
"type": "string",
|
||||
"enum": ["none", "loopback", "external"]
|
||||
},
|
||||
"mount": { "type": "boolean" }
|
||||
},
|
||||
"required": [
|
||||
"capability",
|
||||
"roots",
|
||||
"executables",
|
||||
"environment_names",
|
||||
"network",
|
||||
"mount"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{ "type": "null" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["capability", "requested", "missing", "ceiling"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["code", "category", "message", "retryable", "remediation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -552,7 +371,6 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"invalid_input",
|
||||
"permission_required",
|
||||
"unsupported_provider",
|
||||
"integrity_mismatch",
|
||||
"truncated",
|
||||
@@ -566,12 +384,8 @@
|
||||
"retryable": { "type": "boolean" },
|
||||
"remediation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": { "type": "string", "minLength": 1 },
|
||||
"restart_required": { "type": "boolean" },
|
||||
"elicitation_supported": { "type": "boolean" }
|
||||
},
|
||||
"required": ["action", "restart_required"],
|
||||
"properties": { "action": { "type": "string", "minLength": 1 } },
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"details": {
|
||||
@@ -591,7 +405,6 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"invalid_input",
|
||||
"permission_required",
|
||||
"unsupported_provider",
|
||||
"integrity_mismatch",
|
||||
"truncated",
|
||||
@@ -605,12 +418,8 @@
|
||||
"retryable": { "type": "boolean" },
|
||||
"remediation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": { "type": "string", "minLength": 1 },
|
||||
"restart_required": { "type": "boolean" },
|
||||
"elicitation_supported": { "type": "boolean" }
|
||||
},
|
||||
"required": ["action", "restart_required"],
|
||||
"properties": { "action": { "type": "string", "minLength": 1 } },
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"details": {
|
||||
@@ -630,7 +439,6 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"invalid_input",
|
||||
"permission_required",
|
||||
"unsupported_provider",
|
||||
"integrity_mismatch",
|
||||
"truncated",
|
||||
@@ -644,12 +452,8 @@
|
||||
"retryable": { "type": "boolean" },
|
||||
"remediation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": { "type": "string", "minLength": 1 },
|
||||
"restart_required": { "type": "boolean" },
|
||||
"elicitation_supported": { "type": "boolean" }
|
||||
},
|
||||
"required": ["action", "restart_required"],
|
||||
"properties": { "action": { "type": "string", "minLength": 1 } },
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"details": {
|
||||
@@ -669,7 +473,6 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"invalid_input",
|
||||
"permission_required",
|
||||
"unsupported_provider",
|
||||
"integrity_mismatch",
|
||||
"truncated",
|
||||
@@ -683,12 +486,8 @@
|
||||
"retryable": { "type": "boolean" },
|
||||
"remediation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": { "type": "string", "minLength": 1 },
|
||||
"restart_required": { "type": "boolean" },
|
||||
"elicitation_supported": { "type": "boolean" }
|
||||
},
|
||||
"required": ["action", "restart_required"],
|
||||
"properties": { "action": { "type": "string", "minLength": 1 } },
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"details": {
|
||||
@@ -708,7 +507,6 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"invalid_input",
|
||||
"permission_required",
|
||||
"unsupported_provider",
|
||||
"integrity_mismatch",
|
||||
"truncated",
|
||||
@@ -722,12 +520,8 @@
|
||||
"retryable": { "type": "boolean" },
|
||||
"remediation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": { "type": "string", "minLength": 1 },
|
||||
"restart_required": { "type": "boolean" },
|
||||
"elicitation_supported": { "type": "boolean" }
|
||||
},
|
||||
"required": ["action", "restart_required"],
|
||||
"properties": { "action": { "type": "string", "minLength": 1 } },
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"details": {
|
||||
@@ -747,7 +541,6 @@
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"invalid_input",
|
||||
"permission_required",
|
||||
"unsupported_provider",
|
||||
"integrity_mismatch",
|
||||
"truncated",
|
||||
@@ -761,51 +554,8 @@
|
||||
"retryable": { "type": "boolean" },
|
||||
"remediation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": { "type": "string", "minLength": 1 },
|
||||
"restart_required": { "type": "boolean" },
|
||||
"elicitation_supported": { "type": "boolean" }
|
||||
},
|
||||
"required": ["action", "restart_required"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"details": {
|
||||
"type": "object",
|
||||
"propertyNames": { "type": "string" },
|
||||
"additionalProperties": { "$ref": "#/$defs/__schema0" }
|
||||
}
|
||||
},
|
||||
"required": ["code", "category", "message", "retryable", "remediation"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": { "type": "string", "const": "plan_stale" },
|
||||
"category": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"invalid_input",
|
||||
"permission_required",
|
||||
"unsupported_provider",
|
||||
"integrity_mismatch",
|
||||
"truncated",
|
||||
"cancelled",
|
||||
"timeout",
|
||||
"unavailable",
|
||||
"execution_failure"
|
||||
]
|
||||
},
|
||||
"message": { "type": "string", "minLength": 1 },
|
||||
"retryable": { "type": "boolean" },
|
||||
"remediation": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": { "type": "string", "minLength": 1 },
|
||||
"restart_required": { "type": "boolean" },
|
||||
"elicitation_supported": { "type": "boolean" }
|
||||
},
|
||||
"required": ["action", "restart_required"],
|
||||
"properties": { "action": { "type": "string", "minLength": 1 } },
|
||||
"required": ["action"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"details": {
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
# DOS MZ and COM analysis with Ghidra
|
||||
|
||||
REA can analyze DOS MZ executables and explicitly selected COM images through the bring-your-own Ghidra adapter.
|
||||
The Linux x64 verification lane exercises actual 16-bit disassembly and
|
||||
decompilation through the CLI and stdio MCP. It requires the same Ghidra
|
||||
12.1.4 and 64-bit JDK 21 installation as other Ghidra sessions; no DOS emulator
|
||||
or cross-compiler is required.
|
||||
|
||||
## Open and inspect
|
||||
|
||||
```bash
|
||||
rea inspect /absolute/path/to/legacy.exe --provider ghidra --json
|
||||
rea search /absolute/path/to/legacy.exe entry --kind procedures --provider ghidra --json
|
||||
rea instructions /absolute/path/to/legacy.exe entry --provider ghidra --json
|
||||
rea function /absolute/path/to/legacy.exe entry --provider ghidra --json
|
||||
rea inspect-native-load-image /absolute/path/to/legacy.exe --provider ghidra --json
|
||||
rea read-bytes /absolute/path/to/legacy.exe 0x10000 --length 16 --provider ghidra --json
|
||||
rea address-to-file-offset /absolute/path/to/legacy.exe 0x10000 --provider ghidra --json
|
||||
```
|
||||
|
||||
Use an observed procedure name or address from discovery when the target does
|
||||
not name its entry function `entry`. MCP uses `open_binary`, `list_procedures`,
|
||||
`procedure_info`, `read_function_instructions`, and `analyze_function` with the
|
||||
same admitted target and provider semantics.
|
||||
|
||||
The classifier validates MZ page counts, initialized module length, header and
|
||||
relocation-table bounds, relocation destinations, and the initialized entry
|
||||
point. It distinguishes DOS load-module and relocation bytes from the Windows
|
||||
new-header field. PE remains PE; NE, LE, and LX declarations are explicitly
|
||||
unsupported. A damaged declared Windows header does not silently become DOS.
|
||||
Inventory classification returns `unknown` when its bounded prefix cannot
|
||||
establish the format; this is distinct from the complete target admission check.
|
||||
|
||||
Ghidra imports a private target snapshot with `MzLoader`, language
|
||||
`x86:LE:16:Real Mode`, and compiler specification `default`. These settings,
|
||||
the fixed Ghidra load segment `0x1000`, and the linear address convention are
|
||||
committed in the analysis profile. The handshake rejects language or compiler
|
||||
drift. Closing the session deletes the temporary project and snapshot.
|
||||
|
||||
The profile also commits `function_body_evidence: complete-inclusive-ranges-v1`,
|
||||
so snapshots from older length-only Ghidra results do not satisfy the current
|
||||
profile.
|
||||
|
||||
## Headerless COM images
|
||||
|
||||
COM has no identifying header. Select its interpretation explicitly; neither the
|
||||
`.com` suffix nor arbitrary unrecognized bytes are enough to admit it.
|
||||
|
||||
```bash
|
||||
rea inspect /absolute/path/to/legacy.com --target-format dos-com --provider ghidra --json
|
||||
rea function /absolute/path/to/legacy.com 0x10100 --target-format dos-com --provider ghidra --json
|
||||
rea inspect-native-load-image /absolute/path/to/legacy.com --target-format dos-com --provider ghidra --json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "open_binary",
|
||||
"arguments": {
|
||||
"path": "/absolute/path/to/legacy.com",
|
||||
"format": "dos-com",
|
||||
"provider_id": "ghidra"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The caller's interpretation applies to the complete file, even if its filename
|
||||
suggests another format. Files must contain 1..65280 bytes: the 64 KiB COM segment
|
||||
minus the 256-byte PSP prefix. Ghidra uses `BinaryLoader` with real-mode language
|
||||
and maps file offset zero at `1000:0100` (linear `0x10100`). A packaged pre-analysis
|
||||
script seeds the entry function and CS/DS/ES/SS context `0x1000`. These settings
|
||||
and the preparation policy are committed in the analysis profile; a failed
|
||||
preparation prevents the resident bridge from serving results.
|
||||
|
||||
Load-image verification checks all original and modified file bytes, complete
|
||||
source mapping/memory digests, absence of relocations, entry and measured register
|
||||
context. The context is imposed for analysis, not observed from execution. PSP
|
||||
memory, initial stack contents/SP, DOS interrupts and PC-98 devices are not
|
||||
modeled. COM has no MZ header, relocation table or appended-overlay model; all
|
||||
selected bytes belong to its analysis module. The original file stays unchanged.
|
||||
|
||||
## Addresses and function extent
|
||||
|
||||
Returned default-space addresses are linear byte coordinates. A Ghidra
|
||||
segment:offset address has linear value `segment * 16 + offset`; multiple
|
||||
segment aliases can identify the same byte. REA accepts the returned linear
|
||||
address for subsequent lookups and leaves instruction decoding context with
|
||||
Ghidra. These coordinates are analysis load addresses, not file offsets or
|
||||
observations of a running DOS system.
|
||||
|
||||
`procedure_info` and function dossiers include `body` evidence:
|
||||
|
||||
```json
|
||||
{
|
||||
"available": true,
|
||||
"provenance": "ghidra-function-body-address-set",
|
||||
"ranges": [
|
||||
{ "start": "0x10000", "end": "0x1000d" },
|
||||
{ "start": "0x10060", "end": "0x10065" }
|
||||
],
|
||||
"total_bytes": 20,
|
||||
"span_bytes": 102,
|
||||
"non_contiguous": true,
|
||||
"contains_entry": true
|
||||
}
|
||||
```
|
||||
|
||||
Each end is **inclusive**. `total_bytes` counts the complete observed
|
||||
AddressSet; `span_bytes` measures its enclosing span and is null across address
|
||||
spaces. Gaps and shared tails must not be treated as owned bytes. This is
|
||||
Ghidra's database observation, not a proof of original source ownership or
|
||||
exhaustive code discovery. Providers that do not report complete ranges return
|
||||
`available: false` and an explicit reason.
|
||||
|
||||
## Loaded-image evidence
|
||||
|
||||
`inspect_native_load_image` independently checks the immutable session snapshot
|
||||
against the loaded Ghidra Program. It measures complete original and modified
|
||||
FileBytes digests, every source mapping and its memory digest, relocation records
|
||||
(including multiplicity), and external entry points. It verifies MZ header and
|
||||
module partitions, relocation fixups and linear coordinates rather than relying
|
||||
on the imported executable SHA alone.
|
||||
|
||||
The result is `verified`, `mismatch`, or `unsupported`. Every check retains its
|
||||
expected and observed values. A memory-digest mismatch identifies the source
|
||||
range's file offset and analysis address; it does not locate the first differing
|
||||
byte inside that range. Mapping ends are **inclusive**, unlike the exclusive ends
|
||||
from `list_segments`. Appended overlays remain in source-file identity and are
|
||||
excluded from initialized module coverage. Uninitialized allocations are reported;
|
||||
their contents and DOS runtime semantics are not verified. Formats other than MZ and explicit COM return
|
||||
`unsupported` with measured observations, not a successful MZ verification.
|
||||
|
||||
`read_bytes` returns initialized provider memory, including loader fixups; an
|
||||
unmapped or uninitialized byte ends the read with an explicit returned length and
|
||||
`complete: false`. `address_to_file_offset` uses the provider's source mapping;
|
||||
unmapped, uninitialized and ambiguous mappings fail explicitly. A relocated word
|
||||
can differ from the original file word even when its source offset is known.
|
||||
|
||||
## Coverage boundaries
|
||||
|
||||
Static decompilation does not execute the target or emulate BIOS, DOS
|
||||
interrupts, device ports, or self-modifying code. PC-98 hardware behavior and
|
||||
recovered calling conventions can remain uncertain; provider pseudocode and
|
||||
diagnostics are retained for analyst review.
|
||||
|
||||
For a packed executable, importing the original can analyze its unpacking
|
||||
stub. Analyze a separately prepared unpacked copy to inspect the original
|
||||
program's recovered code. Retain both artifact digests and the transformation
|
||||
evidence; REA never silently replaces the admitted target. Appended overlays
|
||||
are separate from the initialized MZ module. Raw COM files and DOS extenders
|
||||
without an admitted MZ real-mode entry are outside this import boundary.
|
||||
|
||||
DOS MZ is available through Ghidra, with no Hopper support claim. It is outside
|
||||
the Windows native PE P0 boundary. macOS DOS verification remains unverified.
|
||||
|
||||
## Real verification
|
||||
|
||||
```bash
|
||||
npm run verify:ghidra:dos
|
||||
```
|
||||
|
||||
The lane generates a small public fixture from source at runtime. It verifies
|
||||
16-bit instruction bytes, a near call, a segment-relocated far call, actual
|
||||
decompilation, disjoint function ranges, stable CLI/MCP observations, unchanged
|
||||
input bytes, and session/process cleanup. It does not require external
|
||||
executable fixtures, an emulator, or an existing analysis database.
|
||||
|
||||
Provider p-code representations can contain process-specific address-space selector
|
||||
tokens. The lane preserves those reported values and compares stable dossier
|
||||
observations rather than asserting cross-process identity of raw p-code.
|
||||
|
||||
## Function annotations
|
||||
|
||||
`annotate_native_function` changes the name and/or comments at one exact local function entry, verifies readback, and returns a refreshed dossier. This is useful when recording recovered DOS function roles; it does not claim original-source names. Name writes preserve the existing namespace; readback returns the fully qualified name used by function inventories. Regular comments map to Ghidra PRE comments and inline comments to EOL comments. Empty text clears a comment; omitted fields preserve it. The whole edit rolls back if a setter, readback or analysis fails.
|
||||
|
||||
```json
|
||||
{
|
||||
"procedure": "0x10100",
|
||||
"name": "entry",
|
||||
"comment": "Recovered startup role",
|
||||
"inline_comment": "Inspect segment setup"
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
rea annotate-native-function /local/program.com 0x10100 --target-format dos-com --provider ghidra --name entry --comment 'Recovered startup role' --json
|
||||
```
|
||||
|
||||
The CLI returns the updated analysis before closing its isolated session. MCP edits remain visible to later reads until `close_binary`. Original bytes remain unchanged, database edits are discarded on close, and immutable analysis snapshots are invalidated after edits. This operation does not save a Ghidra project, change ABI/types/body ranges, patch instructions, or emulate a DOS runtime. Windows P0 does not admit it.
|
||||
+141
-92
@@ -4,22 +4,27 @@ REA separates installing its CLI from configuring external software and agents.
|
||||
|
||||
## Start setup
|
||||
|
||||
The recommended setup entrypoint is:
|
||||
Start setup with:
|
||||
|
||||
```bash
|
||||
npx --yes rea-agents@latest setup
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
If npm asks to download and run the package, that approval applies only to the
|
||||
current package-runner invocation. REA still prints its own setup plan and asks
|
||||
for separate approval before changing agent configuration or installing a
|
||||
product-owned component.
|
||||
If npm asks to download and run REA, that approval applies only to downloading
|
||||
the package. REA shows its own plan and asks before changing agent configuration
|
||||
or installing Hopper.
|
||||
|
||||
The explicit `@latest` request prevents a project dependency or stale npx cache
|
||||
entry from choosing the setup version.
|
||||
REA runs the version npm selected and never silently replaces an intentional
|
||||
version request after launch. To repair registrations written by an older
|
||||
release, rerun the command above and review the resulting setup plan.
|
||||
The short command can use a REA version installed in the current project. To
|
||||
request the latest release explicitly, use:
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
REA runs the version npm selects. To update older agent registrations, run the
|
||||
latest-version command and review its setup plan. For unattended package
|
||||
downloads, add `--yes` before the package name; this does not approve REA's
|
||||
setup changes.
|
||||
|
||||
For an intentional rollback, make the package request explicit:
|
||||
|
||||
@@ -31,11 +36,11 @@ Setup continues to pin persistent MCP registrations to the exact version that
|
||||
performed setup. Running current setup later migrates unversioned or older
|
||||
managed registrations through the normal reviewed setup transaction.
|
||||
|
||||
REA supports Node.js 22.19+ and 24.11+ (including newer releases). It uses the npm already paired with that runtime and never upgrades Node.js, npm, or Homebrew.
|
||||
REA supports Node.js 22.x (>=22.19), 24.x (>=24.11), and 26+. Node.js 23, 25, and prereleases are unsupported. It uses the npm already paired with that runtime and never upgrades Node.js, npm, or Homebrew.
|
||||
|
||||
Running `npm install rea-agents` without `--global` installs the executable only
|
||||
in the current project's `node_modules/.bin`; it does not make `rea` available
|
||||
on the shell `PATH`. Use the explicit setup command above for the guided setup
|
||||
on the shell `PATH`. Use the setup command above for the guided setup
|
||||
journey, `npx -y rea-agents@latest` for unattended one-off commands, or install globally
|
||||
with `npm install --global rea-agents` for a shell-visible `rea` command.
|
||||
|
||||
@@ -56,60 +61,65 @@ curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh |
|
||||
|
||||
Supported options are `--version <semver>`, `--dry-run`, `--no-setup`, `--no-prompt`, and `--verbose`. Neither `--no-prompt` nor a non-interactive shell grants permission to install external dependencies.
|
||||
|
||||
## Supported agents
|
||||
|
||||
Setup can configure these clients for REA's local MCP server:
|
||||
|
||||
| Client | `--client` value |
|
||||
| ------------------ | ---------------- |
|
||||
| Claude Code | `claude_code` |
|
||||
| Claude Desktop | `claude_desktop` |
|
||||
| Codex | `codex` |
|
||||
| Cursor | `cursor` |
|
||||
| Gemini CLI | `gemini_cli` |
|
||||
| Windsurf | `windsurf` |
|
||||
| Devin | `devin` |
|
||||
| OpenCode | `opencode` |
|
||||
| Antigravity | `antigravity` |
|
||||
| GitHub Copilot CLI | `copilot_cli` |
|
||||
| Command Code | `commandcode` |
|
||||
| VS Code | `vscode` |
|
||||
|
||||
## Review setup changes
|
||||
|
||||
`rea setup` uses an inline, scroll-preserving journey inspired by the clarity of
|
||||
PostHog's CLI wizard. It begins with the outcome instead of the installer
|
||||
mechanics:
|
||||
`rea setup` first offers the supported agents in a multi-select. Existing REA
|
||||
registrations are selected by default. Newly detected clients remain available
|
||||
but unselected: detection gives setup context, not permission to add a new
|
||||
registration. Clients without a detected configuration can still be selected.
|
||||
Explicit `--client` flags skip this question.
|
||||
|
||||
- investigate local applications from a supported agent;
|
||||
- recover evidence through an available deep-analysis provider;
|
||||
- use the bundled skill for a repeatable investigation workflow.
|
||||
Setup adds MCP access for selected clients. It installs REA's bundled workflow
|
||||
with those integrations by default; use `--skill=false` to omit it. If no agent
|
||||
is selected, setup offers the workflow separately for CLI use, with No as the
|
||||
default. Hopper is a separate optional choice: setup shows its proposed
|
||||
installation or connection and requires its own explicit approval. It can also
|
||||
save verified paths for an existing Ghidra installation.
|
||||
|
||||
REA then summarizes the detected clients and asks which capabilities to set up.
|
||||
The capability picker pre-selects `Agent integration` (the MCP registration and
|
||||
matching guided workflow together) when detected agents exist, and presents the
|
||||
optional Hopper provider alongside it. Selecting agent integration opens a second
|
||||
checklist for the exact detected agents that should receive a registration; all
|
||||
detected agents are pre-selected. Detection provides context and pre-selection;
|
||||
it does not authorize a configuration write until the user confirms.
|
||||
After selection, review the plan's exact paths and changes and approve before
|
||||
REA writes files or installs Hopper. You can cancel at any prompt.
|
||||
|
||||
Because the user has explicitly invoked `rea setup`, the wizard biases toward the
|
||||
happy path: capabilities and agents are pre-selected, and the final approval
|
||||
defaults to **Yes**. The user can still deselect any item or cancel at any prompt.
|
||||
Choosing no capabilities exits without changes. Selecting agent integration
|
||||
always includes the bundled skill, even if all agents are deselected. The picker
|
||||
keeps its navigation, selection, confirmation, and cancellation keys visible
|
||||
instead of relying on a transient hint.
|
||||
Before applying changes, REA checks your current configuration. The plan lists:
|
||||
|
||||
Every selected path converges on the same exact preflight. REA validates the
|
||||
current state, prints the proposed effects, and asks for final approval with
|
||||
**Yes** as the default. Selection alone never authorizes a mutation. The plan
|
||||
identifies:
|
||||
|
||||
- an existing Hopper installation, a validated bring-your-own Ghidra environment, or the official Hopper package it proposes to install;
|
||||
- an existing Hopper installation, a verified existing Ghidra installation, or the official Hopper package it proposes to install;
|
||||
- each detected agent configuration path;
|
||||
- the REA skill destination;
|
||||
- external software, network origins, integrity evidence, and package-manager
|
||||
commands.
|
||||
|
||||
Malformed or unsafe existing configuration blocks the whole transaction before
|
||||
Hopper installation or any file write. Declining, pressing Ctrl-C, or selecting
|
||||
nothing makes no changes. Agent configuration writes preserve unrelated
|
||||
Hopper installation or any file write. Declining or pressing Ctrl-C makes no
|
||||
changes. Agent configuration writes preserve unrelated
|
||||
entries, create backups, use atomic replacement, and verify their result.
|
||||
|
||||
Progress remains append-only so completed and failed operations stay visible in
|
||||
terminal history. After a successful run, the completion message names the
|
||||
verified capabilities now available—for example configured MCP clients, the
|
||||
selected analysis provider, and the installed skill—and gives the corresponding
|
||||
next action. When an agent must restart to load its registration, REA says so;
|
||||
otherwise it suggests beginning an investigation. It does not advertise a
|
||||
capability that the final diagnostic check did not verify.
|
||||
After setup, REA reports which agents, analysis tools, and workflow files passed
|
||||
its final checks. Restart any agent named in the completion message, then begin
|
||||
your investigation. Failed steps and diagnostics remain in terminal history.
|
||||
|
||||
Select exact clients in scripts with repeatable `--client` flags, or retain
|
||||
automatic discovery explicitly with `--all-detected`. Use `--skill=false` to
|
||||
override the normal bundled-skill installation and `--dry-run` for a read-only
|
||||
plan:
|
||||
Select exact clients in scripts with repeatable `--client` flags. Each explicit
|
||||
client skips interactive selection. Use `--all-detected` only when you intend
|
||||
to configure every detected supported client. `--skill=false` omits the
|
||||
workflow, and `--dry-run` returns a read-only plan with status `planned` and
|
||||
exit code 0:
|
||||
|
||||
```bash
|
||||
rea setup --client codex --client cursor --skill=false --dry-run
|
||||
@@ -117,13 +127,20 @@ rea setup --client codex --client cursor --skill=false --dry-run
|
||||
|
||||
Prompt UI and progress are written to stderr so stdout remains available for
|
||||
structured results and pipelines. `NO_COLOR=1` disables color. Use
|
||||
`--accessible` for sequential, vertically rendered yes/no prompts.
|
||||
`--accessible` for sequential, vertically rendered yes/no prompts. Implicit
|
||||
interactive setup requires stdin, stdout, and stderr to all be terminals; when
|
||||
any stream is redirected, setup stays non-interactive. Declining or cancelling
|
||||
returns status `cancelled` with exit code 0.
|
||||
|
||||
For automation, `rea setup --json` reports the plan without applying it.
|
||||
Prefer pairing `--yes` with explicit scope such as `--client codex`,
|
||||
or `--all-detected`. Legacy unscoped `--yes` remains compatible for
|
||||
this release but emits a deprecation warning. Installing missing Hopper
|
||||
non-interactively additionally requires `--install-hopper`:
|
||||
For automation, `rea setup --json` reports the plan and a compact `.doctor`
|
||||
readiness projection without applying it. Use `rea doctor --json` for full
|
||||
health diagnostics and canonical tool catalog details. Pair
|
||||
`--yes` with explicit scope such as `--client codex`, or use
|
||||
`--all-detected` when the broad scope is intended. Without a scope flag,
|
||||
`--yes` is limited to existing REA-owned registrations; it does not select all
|
||||
detected clients. An unapproved actual apply reports `needs_confirmation` and
|
||||
exits 1. Installing missing Hopper non-interactively additionally requires
|
||||
`--install-hopper`:
|
||||
|
||||
```bash
|
||||
rea setup --yes --all-detected --install-hopper --json
|
||||
@@ -131,11 +148,14 @@ rea setup --yes --all-detected --install-hopper --json
|
||||
|
||||
Setup pins package-runner MCP registrations to the exact installed REA version,
|
||||
installs the matching skill and on-demand references in the same plan, and adds
|
||||
`startup_timeout_sec = 30` for Codex. Interactive `rea upgrade` installs the new
|
||||
executable and then opens the updated `rea setup --all-detected` plan for
|
||||
separate approval. Structured or non-TTY upgrades defer that integration sync.
|
||||
Use the same setup command to migrate floating, unversioned, or stale
|
||||
registrations, then restart changed clients.
|
||||
`startup_timeout_sec = 30` for Codex. `rea update` installs the exact resolved
|
||||
release into the npm prefix that owns the running package, then checks the new
|
||||
executable's version before reporting success. It does not reopen onboarding.
|
||||
Release lookup and installation both use npm's configured registry.
|
||||
Its maintenance plan selects only existing REA registrations and an already
|
||||
installed REA skill. Run the returned scoped setup command to review and approve
|
||||
those changes, then restart affected agents. The plan is returned in terminal,
|
||||
non-TTY, and JSON modes without applying configuration changes.
|
||||
|
||||
## Hopper
|
||||
|
||||
@@ -193,15 +213,28 @@ copying license secrets into logs, or killing unrelated Hopper processes.
|
||||
|
||||
## Ghidra
|
||||
|
||||
REA's Ghidra provider is bring-your-own and supports Linux x64 with the exact
|
||||
official Ghidra 12.1.4 release and a 64-bit full JDK 21. macOS is not an
|
||||
admitted Ghidra host. An experimental
|
||||
Windows x64 P0 supports approved native x86-64 PE applications. It supplies
|
||||
discovery, analysis-profile commitment, an isolated read-only headless session,
|
||||
ten inventory/name/search operations and nine function-analysis operations,
|
||||
for 19 read-only operations total. Function analysis covers metadata,
|
||||
decompilation, assembly, resolved calls, typed references, xrefs, CFG, and
|
||||
dossiers. GUI state and analysis mutations remain unavailable through Ghidra.
|
||||
REA connects to an existing Ghidra installation on Linux x64, macOS x64/arm64,
|
||||
or experimental Windows x64 P0.
|
||||
It requires Ghidra 12.1.4 and a 64-bit full JDK 21. On macOS, the installation
|
||||
must include the native decompiler for the host architecture; REA does not
|
||||
build it or change Gatekeeper quarantine settings.
|
||||
|
||||
The adapter exposes 25 read-only operations: thirteen inventory/name/search
|
||||
operations and twelve function-analysis operations. These cover metadata,
|
||||
decompilation, assembly, resolved calls, typed references, xrefs, function
|
||||
dossiers, instructions, recovered data types, measured load mappings, loaded
|
||||
memory bytes, and observed file offsets. Independent load-image attestation
|
||||
supports DOS MZ and explicitly selected COM; PE returns its measurements with that limitation.
|
||||
On Linux and macOS, `annotate_native_function` also edits a function name and/or
|
||||
entry comments atomically and returns refreshed analysis. These session metadata
|
||||
edits leave executable bytes unchanged and are discarded on close. GUI controls
|
||||
require Hopper; Windows P0 remains read-only.
|
||||
|
||||
Windows P0 admits native x86-64 PE applications on fixed local NTFS volumes.
|
||||
The npm package bundles native Job Object ownership, protected private runtime
|
||||
DACLs, and handle-based path admission; no separate addon installation is needed.
|
||||
See the [Windows Ghidra P0 guide](windows-ghidra-p0.md) for verified scope and
|
||||
limitations.
|
||||
|
||||
Extract Ghidra and install the JDK outside REA, then export absolute paths:
|
||||
|
||||
@@ -212,7 +245,7 @@ rea doctor --json
|
||||
rea setup
|
||||
```
|
||||
|
||||
PowerShell configuration for Windows uses the same non-secret paths:
|
||||
On Windows, configure the existing installation in PowerShell:
|
||||
|
||||
```powershell
|
||||
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
|
||||
@@ -221,10 +254,12 @@ rea doctor --json
|
||||
rea providers --json
|
||||
```
|
||||
|
||||
`rea setup` does not mutate Windows client configuration or install Hopper,
|
||||
Ghidra, Java, Python, or another package. Register the built `rea mcp` command
|
||||
manually in the intended client and preserve the two environment variables.
|
||||
See [Windows Ghidra P0](windows-ghidra-p0.md) for a complete example.
|
||||
`rea setup` can configure supported Windows agents and install the bundled
|
||||
REA skill after approval. Direct registrations use Node to launch REA's entry
|
||||
script; package-runner registrations use the pinned `npx` command. Hopper
|
||||
installation remains unavailable on Windows. Setup never installs Ghidra,
|
||||
Java, or Python. It preserves valid detected Ghidra/JDK settings in agent
|
||||
registrations. See [Windows Ghidra P0](windows-ghidra-p0.md) for provider diagnostics.
|
||||
|
||||
Doctor validates the platform, architecture, application version,
|
||||
`support/analyzeHeadless` or `support/analyzeHeadless.bat`, Java
|
||||
@@ -237,12 +272,10 @@ installs, upgrades, or modifies Ghidra or Java.
|
||||
Each verified session uses an ephemeral temporary project and isolated
|
||||
home/cache/config/temp paths. REA passes `-readOnly`, `-deleteProject`, uses
|
||||
Ghidra's default analysis and resource settings, and loads its packaged Java
|
||||
bridge via `-scriptPath`; it never opens an existing user project. Linux uses a
|
||||
current-user-only local bridge socket and descriptor. Windows P0 uses
|
||||
token-authenticated IPv4 loopback, a token-free endpoint record, and bounded
|
||||
process-tree termination. It does not yet prove private DACL,
|
||||
reparse-point-safe, or Job Object semantics; use only approved non-sensitive
|
||||
fixtures.
|
||||
bridge via `-scriptPath`; it never opens an existing user project. Linux and
|
||||
macOS use a current-user-only local bridge socket and descriptor. The
|
||||
experimental Windows transport uses authenticated IPv4 loopback with a
|
||||
private native-owned bearer descriptor and Job Object process ownership.
|
||||
|
||||
Operations begin only after default auto-analysis completes. The provider
|
||||
startup deadline fails the open rather than exposing partial analysis. One
|
||||
@@ -255,9 +288,18 @@ is no fixed per-operation or response-size ceiling. Unresolved computed calls
|
||||
remain unknown, reference-kind provenance is preserved, and provider-specific
|
||||
pseudocode is never treated as original source or Hopper-equivalent text.
|
||||
|
||||
Unexpected request failures retain their original internal cause. CLI and MCP
|
||||
errors expose Error names, messages, and available codes under
|
||||
`details.diagnostics.failure_cause`; other rejection values retain their
|
||||
primitive value or an explicit type. Shutdown warnings include the failure
|
||||
kind, message, and diagnostics, while process and temporary-project cleanup
|
||||
continues. These messages redact known bridge authentication tokens and
|
||||
preserve local paths and other analysis context.
|
||||
|
||||
Run `GHIDRA_INSTALL_DIR=... npm run verify:ghidra` from a source checkout to
|
||||
compile and analyze debug and stripped x86-64 ELF fixtures on Linux x64. This lane needs only a host C compiler
|
||||
in addition to Ghidra and its JDK.
|
||||
compile and analyze debug and stripped host-native fixtures (ELF on Linux x64
|
||||
or Mach-O on macOS), plus a native DWARF 4 type-layout object. This lane needs a
|
||||
host C compiler in addition to Ghidra and its JDK.
|
||||
|
||||
Run `GHIDRA_INSTALL_DIR=... npm run verify:ghidra:cross-format` to add AArch64
|
||||
ELF, x86-64 PE, and x86-64 Mach-O fixture coverage. This separate lane needs
|
||||
@@ -267,12 +309,14 @@ host-native Ghidra acceptance lane.
|
||||
|
||||
On a controlled Windows x64 runner, use
|
||||
`npm run verify:ghidra:windows`. The verifier generates a deterministic native
|
||||
PE fixture from source bytes, exercises every admitted Ghidra operation, checks
|
||||
target/snapshot/import digest identity, and requires complete runtime cleanup.
|
||||
PE fixture from source bytes and requires the Windows native authority before
|
||||
opening the provider. This lane remains blocked until those controls are
|
||||
implemented; its intended checks include operation coverage, digest identity,
|
||||
and complete runtime cleanup.
|
||||
|
||||
## Diagnose, update, and remove
|
||||
|
||||
`rea doctor --json` is strictly read-only. `rea upgrade` updates only the npm installation that owns the running CLI. `rea uninstall` removes only REA-owned agent registrations and skill files; `--purge-data` additionally removes REA cache and state paths.
|
||||
`rea doctor --json` is strictly read-only. `rea update` updates only the npm installation that owns the running CLI. Source checkouts and package-runner copies must be updated through the mechanism that owns them; a fresh package-runner invocation can use `npx rea-agents@latest`. `rea uninstall` removes only REA-owned agent registrations and skill files; `--purge-data` additionally removes REA cache and state paths.
|
||||
|
||||
## MCP Registry
|
||||
|
||||
@@ -286,17 +330,22 @@ server through `npx` with the `mcp` command.
|
||||
|
||||
For a client that requires manual configuration, use:
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@latest", "mcp"]
|
||||
"args": ["-y", "rea-agents@4.0.1", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use an exact `rea-agents@VERSION` in the arguments when a reproducible client
|
||||
configuration is required. `rea setup` writes the same package-runner shape and
|
||||
pins it to the exact version that performed setup.
|
||||
<!-- x-release-please-end -->
|
||||
|
||||
Persistent registrations should use one exact package version. `rea setup`
|
||||
writes the same package-runner shape and pins it to the exact version that
|
||||
performed setup. Run current setup to refresh an older registration, then
|
||||
restart the client.
|
||||
|
||||
@@ -97,7 +97,7 @@ correspondence is a separately identified cross-layer inference.
|
||||
Every observation and edge carries all of the following:
|
||||
|
||||
- an authority, such as artifact bytes, AST analysis, static relationship
|
||||
inference, passive CDP runtime, cross-layer reconciliation, controlled replay,
|
||||
inference, passive CDP runtime, and cross-layer reconciliation,
|
||||
native analysis, historical reference, or user assertion;
|
||||
- an epistemic state: `observed`, `inferred`, `unknown`, or `unavailable`;
|
||||
- confidence independently bounded as exact, high, medium, low, or unknown;
|
||||
|
||||
@@ -7,7 +7,7 @@ are `trace_application_feature`, `trace_javascript_semantics`,
|
||||
`compare_javascript_export_shapes`; their CLI equivalents use the same names
|
||||
with hyphens.
|
||||
|
||||
Both workflows consume Evidence produced by
|
||||
These workflows consume Evidence produced by
|
||||
`analyze_javascript_application` or `reconcile_javascript_runtime`. They do not
|
||||
read an artifact, execute application code, attach to a process, or open a
|
||||
native-analysis provider. Static artifact observations, passive runtime
|
||||
@@ -132,8 +132,10 @@ unique occurrence on each side and pairing is reciprocal. Changes use JSON
|
||||
Pointer paths with `added`, `removed`, `changed`, or `unknown` status. A missing
|
||||
field is added or removed only when the relevant parent-property coverage is
|
||||
complete on both shapes. The output includes exact selector candidates,
|
||||
omissions, Evidence links, coverage, limitations, and a separate controlled
|
||||
replay recommendation; it does not execute JavaScript.
|
||||
omissions, Evidence links, coverage, and limitations; it does not execute
|
||||
JavaScript. When runtime semantics matter, run behavioral probes directly
|
||||
against the relevant application versions and capture them through the
|
||||
available browser, Electron, or process workflows.
|
||||
|
||||
## CLI and verification
|
||||
|
||||
@@ -146,8 +148,7 @@ For two operator-provided directories or ASARs, run:
|
||||
```bash
|
||||
npm run verify:application-workflows -- \
|
||||
--left /absolute/path/to/version-a \
|
||||
--right /absolute/path/to/version-b \
|
||||
--source-map-read-approved
|
||||
--right /absolute/path/to/version-b
|
||||
```
|
||||
|
||||
The verifier reconstructs both versions independently, compares them, runs one
|
||||
@@ -163,23 +164,10 @@ Because source Evidence and derived Evidence are retained by the normal session
|
||||
ledger, evidence bundles and analysis snapshots can carry these records without
|
||||
another persistence format.
|
||||
|
||||
## Controlled replay boundary
|
||||
## Runtime evidence
|
||||
|
||||
Static graph workflows never execute a graph node or recovered module.
|
||||
`run_controlled_replay` is the separate extracted-module boundary: it requires
|
||||
the `javascript_replay` authority, a plan call followed by exact content-bound
|
||||
approval, and the mandatory Linux OS sandbox fixed by
|
||||
[ADR-0002](adr/0002-controlled-replay-authority-and-sandbox.md). Browser,
|
||||
Electron, Process Capture, artifact-read, and static-analysis approvals do not
|
||||
authorize that execution.
|
||||
|
||||
Replay observations retain `controlled-replay` authority. They cannot
|
||||
promote static inference into passive runtime observation or prove that the
|
||||
original application, renderer, preload, main process, or remote service
|
||||
behaved identically.
|
||||
|
||||
Module, explicit case, and preset-generated case lists have no fixed
|
||||
item-count ceiling. Replay plans commit the caller's resource budgets, and the
|
||||
worker protocol checks the complete request against that declared byte budget
|
||||
before probing executables or reading modules. Aggregate module-source and
|
||||
case-input bytes are likewise checked against the caller's declared budgets.
|
||||
Static graph workflows do not execute recovered code. When runtime semantics
|
||||
matter, run behavioral probes directly against the relevant application
|
||||
versions and capture them through the available browser, Electron, or process
|
||||
workflows. Those observations do not prove behavior in an unobserved app
|
||||
version or environment.
|
||||
|
||||
@@ -52,6 +52,23 @@ manifest and graph commitments, JavaScript Application Graph, static
|
||||
Electron summary, reconstruction statistics, and explicit limitations. It does
|
||||
not require a live Hopper, Ghidra, browser, or Electron process.
|
||||
|
||||
## Large results
|
||||
|
||||
Graph and Evidence identifiers hash canonical JSON incrementally, without
|
||||
assembling a single string for the whole graph. The canonical bytes and existing
|
||||
identifiers remain unchanged. The opt-in regression check is
|
||||
`npm run verify:javascript:digests`;
|
||||
it hashes a value larger than the running Node engine's single-string limit.
|
||||
|
||||
Output formatters and MCP transport serialization still assemble whole strings.
|
||||
For a large CLI result, select the needed fields
|
||||
before formatting, for example `--format json --filter-output
|
||||
evidence_id,normalized_result.statistics`. Complete serialization of a result
|
||||
beyond the engine's string limit is not established by this digest check.
|
||||
Client framing limits also apply: the pinned Node MCP SDK's stdio transport
|
||||
defaults to a 10 MiB buffer. Its caller-selected `maxBufferSize` must accommodate
|
||||
the complete response, including text and structured Evidence projections.
|
||||
|
||||
## What is reconstructed
|
||||
|
||||
The projector reuses the content-addressed artifact inventory and safe artifact
|
||||
@@ -125,9 +142,10 @@ For `.node` bindings, member names mean “requested by JavaScript syntax.” A
|
||||
resolved add-on path does not convert them into verified native exports. Native
|
||||
symbol verification remains a separate deep-analysis claim.
|
||||
|
||||
Endpoint observations preserve useful local diagnostics while removing URL
|
||||
credentials, fragments, and query values. Artifact paths, digests, parse
|
||||
locations, and analysis metadata remain actionable because REA is local-only.
|
||||
Endpoint observations preserve useful local diagnostics, including query values
|
||||
and fragments, while removing only URL username/password credentials. Artifact
|
||||
paths, digests, parse locations, and analysis metadata remain actionable
|
||||
because REA is local-only.
|
||||
|
||||
## Safety boundary
|
||||
|
||||
|
||||
@@ -7,13 +7,11 @@ have equivalent `rea list-javascript-runtime-targets` and
|
||||
`rea observe-javascript-runtime` commands. Successful calls return
|
||||
deterministic Evidence.
|
||||
|
||||
This authority is disabled by default and is separate from browser CDP,
|
||||
Electron file-page inspection, Process Capture, and controlled JavaScript
|
||||
replay:
|
||||
Each request supplies the loopback Inspector endpoint and, for observation, the
|
||||
target ID. This surface is separate from browser CDP, Electron file-page
|
||||
inspection, and Process Capture:
|
||||
|
||||
```bash
|
||||
export REA_V8_INSPECTOR_OBSERVE_ENABLED=true
|
||||
|
||||
rea list-javascript-runtime-targets http://127.0.0.1:9229 \
|
||||
--json
|
||||
rea observe-javascript-runtime http://127.0.0.1:9229 TARGET_ID \
|
||||
@@ -27,6 +25,38 @@ array.
|
||||
|
||||
For the MCP follow-up call, pass the same literal-loopback endpoint and the selected target ID to `observe_javascript_runtime`. REA rediscovers targets and validates the ID against the live endpoint before observation.
|
||||
|
||||
### Discovery file locations
|
||||
|
||||
Node's `/json/list` reports a best-effort pathname prefixed with `file://`,
|
||||
without URI encoding. On POSIX, REA interprets unambiguous paths literally,
|
||||
including `#` and `%` in filenames. Node also replaces backslashes and double
|
||||
quotes with underscores. That transformation cannot be reversed reliably:
|
||||
an underscore may already be part of the original filename.
|
||||
|
||||
Windows paths, ambiguous underscore paths, and local discovery paths that
|
||||
cannot be verified remain eligible by their endpoint and exact target ID.
|
||||
Their target location preserves the reported value:
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": "unresolved",
|
||||
"reported_url": "file://C:_tools_entry.js",
|
||||
"reason": "unverifiable-file-location"
|
||||
}
|
||||
```
|
||||
|
||||
This location does not claim that a file exists or authorize reading its
|
||||
contents. Listing remains HTTP-only. Observation resolves loaded script
|
||||
locations independently from `Debugger.scriptParsed` file URLs, preserving
|
||||
the existing checks for local files and encoded separators. Unsupported
|
||||
schemes, remote file hosts, and a bare `file://` remain excluded. These Node
|
||||
discovery rules do not apply to page targets or other runtime products.
|
||||
|
||||
Reconciliation retains an unresolved `target_location` object and reports
|
||||
`runtime-location-unresolved` with unknown confidence for that target. It
|
||||
does not derive a static match from the reported pathname; verified loaded
|
||||
script locations remain available for independent matching.
|
||||
|
||||
## Passive protocol boundary
|
||||
|
||||
The provider sends exactly two protocol commands:
|
||||
@@ -66,14 +96,14 @@ Electron role, so Evidence records the role authority as
|
||||
`caller-declared-unverified`.
|
||||
|
||||
Electron main, preload, and renderer behavior can therefore be observed only
|
||||
as separate approved Inspector targets. The provider does not infer that two
|
||||
as separate Inspector targets. The provider does not infer that two
|
||||
targets belong to the same Electron application.
|
||||
|
||||
## Capture and determinism
|
||||
|
||||
Every observation records its time window and returns all valid script and
|
||||
execution-context events received during it. Per-location protocol validation
|
||||
remains in force. Scripts are authorized after capture, deduplicated by stable
|
||||
remains in force. Scripts are validated after capture, deduplicated by stable
|
||||
metadata, and canonically sorted. Wall-clock timestamps and protocol script IDs
|
||||
are excluded from the durable result, so identical inputs and captured metadata
|
||||
produce the same Evidence ID.
|
||||
@@ -88,8 +118,8 @@ behavior does not occur.
|
||||
Pass the resulting observation Evidence and an
|
||||
`analyze_javascript_application` Evidence record to
|
||||
`reconcile_javascript_runtime`. The reconciliation accepts this provider
|
||||
alongside passive web and Electron page captures. Exact approved file or URL
|
||||
mappings can correlate script presence with JavaScript Application Graph
|
||||
alongside passive web and Electron page captures. Exact file or URL mappings
|
||||
supplied with the request can correlate script presence with JavaScript Application Graph
|
||||
assets. Because this provider never reads source bytes, matches normally use a
|
||||
unique authorized location and remain weaker than captured-byte identity.
|
||||
Builtin `node:` scripts remain runtime-only nodes unless a future explicit
|
||||
|
||||
@@ -21,7 +21,7 @@ parameters, and normalized-result schemas are checked before reconciliation.
|
||||
supplied as additional layers.
|
||||
2. Run `inspect_web_page`, `inspect_electron_page`, or the attach-only
|
||||
`observe_javascript_runtime`. Capturing script sources
|
||||
is optional, but an approved source capture provides the strongest byte
|
||||
is optional, but a source capture provides the strongest byte
|
||||
identity.
|
||||
3. Pass the resulting Evidence records to `reconcile_javascript_runtime`.
|
||||
Browser URLs normally need an explicit URL-prefix mapping. Extracted
|
||||
@@ -77,18 +77,16 @@ npm run verify:javascript-runtime -- \
|
||||
--mapping '{"layer":1,"kind":"file-root","root":"/runtime/cache","artifact_prefix":""}'
|
||||
```
|
||||
|
||||
The verifier analyzes the supplied static paths under a temporary in-process
|
||||
permission policy, reads bounded Evidence JSON files, and prints graph IDs,
|
||||
evidence IDs, paths, digests, counts, reasons, and coverage. It does not print
|
||||
captured source text. `--source-map-read-approved` remains a distinct explicit
|
||||
approval.
|
||||
The verifier analyzes the supplied static paths, reads bounded Evidence JSON
|
||||
files, and prints graph IDs, evidence IDs, paths, digests, counts, reasons, and
|
||||
coverage. It does not print captured source text.
|
||||
|
||||
## Matching rules
|
||||
|
||||
Runtime targets, frames, scripts, and workers become capture-scoped graph
|
||||
nodes. REA evaluates compatible static candidates conservatively:
|
||||
|
||||
1. An approved captured-source SHA-256 plus one matching mapped location yields
|
||||
1. A captured-source SHA-256 plus one matching mapped location yields
|
||||
a content-and-location match.
|
||||
2. One unique exact content digest can match even when no location mapping is
|
||||
available. Whole-asset digests and reconstructed module-factory source
|
||||
@@ -144,8 +142,8 @@ EventEmitter activity, Electron IPC, or script-unload evidence. A unique
|
||||
authorized file or URL location can still correlate script presence with a
|
||||
static asset, but it does not prove module initialization or feature execution.
|
||||
|
||||
Source-map declarations and approved original-source reads stay under static
|
||||
source-map authority. They are reported in `source_map_authority` but never used
|
||||
Source-map declarations and original-source reads remain static evidence. They
|
||||
are reported in `source_map_authority` but never used
|
||||
as primary runtime byte matches. A source name or `sourcesContent` digest is not
|
||||
silently equated with the generated bytes observed by CDP.
|
||||
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
# Managed-code analysis plan
|
||||
|
||||
This document turns
|
||||
[ADR-0003](adr/0003-managed-code-evidence-and-provider-boundary.md) into an
|
||||
implementation and verification plan. REA currently ships read-only PE/CLI
|
||||
triage and exact identity through `inspect_managed_artifact` and
|
||||
`rea inspect-managed-artifact`, plus file-backed metadata, signature,
|
||||
method-body CIL, exception-region, call-edge, and field-access inspection
|
||||
through `inspect_managed_members` and `rea inspect-managed-members`, and
|
||||
declared ModuleRef/ImplMap/PInvoke and native implementation boundary inventory
|
||||
through `inspect_managed_native_boundaries` and
|
||||
`rea inspect-managed-native-boundaries`, plus obfuscation-resistant member
|
||||
comparison through `compare_managed_members` and `rea compare-managed-members`,
|
||||
managed/native export or function Evidence matching through
|
||||
`verify_managed_native_boundaries` and `rea verify-managed-native-boundaries`,
|
||||
decompiler reconstruction import through `import_managed_reconstruction` and
|
||||
`rea import-managed-reconstruction`, and default-disabled runtime-correlation
|
||||
admission planning through `plan_managed_runtime_correlation` and
|
||||
`rea plan-managed-runtime-correlation`, plus static managed graph projection
|
||||
through `project_managed_application_graph` and
|
||||
`rea project-managed-application-graph`. Native-body bridge mapping and runtime
|
||||
execution remain planned behavior. The current product inventory remains the
|
||||
one in [`product-catalog.json`](product-catalog.json).
|
||||
REA inspects .NET PE/CLI artifacts without loading or executing their code.
|
||||
It can identify an assembly, inspect metadata and CIL, compare members across
|
||||
builds, and connect declared native calls to supplied native-analysis evidence.
|
||||
CIL is Common Intermediate Language, the instruction format used by managed
|
||||
assemblies.
|
||||
|
||||
The seven shipped tools are:
|
||||
|
||||
| Task | MCP tool |
|
||||
| --------------------------------------------------------------------------- | ----------------------------------- |
|
||||
| Identify an artifact and its managed deployment form | `inspect_managed_artifact` |
|
||||
| Inspect types, signatures, method bodies, calls, and field access | `inspect_managed_members` |
|
||||
| List declared native calls and native implementation indicators | `inspect_managed_native_boundaries` |
|
||||
| Compare members across builds and map build-local tokens | `compare_managed_members` |
|
||||
| Check native call declarations against supplied export or function evidence | `verify_managed_native_boundaries` |
|
||||
| Import decompiled code against verified static member identities | `import_managed_reconstruction` |
|
||||
| Add managed findings to an application graph | `project_managed_application_graph` |
|
||||
|
||||
Each has a matching CLI command: replace underscores with hyphens and prefix
|
||||
the name with `rea`, for example `rea inspect-managed-artifact`.
|
||||
Native-body bridge mapping remains planned; managed runtime execution is not
|
||||
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).
|
||||
|
||||
## Analysis objective
|
||||
|
||||
@@ -32,8 +37,8 @@ collapsing them:
|
||||
3. What source-like or behavioral structure can be reconstructed or inferred?
|
||||
4. Which findings survive an exact-build check or a cross-build structural
|
||||
comparison?
|
||||
5. Which remaining questions require native analysis or a separately
|
||||
authorized runtime experiment?
|
||||
5. Which remaining questions require native analysis or runtime evidence that
|
||||
static inspection cannot supply?
|
||||
|
||||
The ordinary workflow ends at question four. Static analysis never loads or
|
||||
executes the target.
|
||||
@@ -151,7 +156,7 @@ UTF-8 encoding, and lowercase hexadecimal output.
|
||||
### Epistemic commitment
|
||||
|
||||
- authority: static bytes, reconstruction, structural inference, independent
|
||||
validation, native provider, or separately authorized runtime;
|
||||
validation, native provider, or future runtime observation;
|
||||
- state: observed, inferred, unknown, or unavailable;
|
||||
- confidence independent of authority;
|
||||
- coverage and admitted/dropped counts;
|
||||
@@ -216,31 +221,34 @@ The workflow produces separate observation, inference, validation, and unknown
|
||||
tables. It may say that a method is a strong candidate for a role; it cannot
|
||||
turn that role into the method's durable identity.
|
||||
|
||||
Cross-version matching is two-stage:
|
||||
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.
|
||||
|
||||
1. Exact identity requires compatible artifact/module commitments and exact
|
||||
raw CIL identity; no standalone complete-method-body digest is exposed.
|
||||
2. Structural identity compares normalized signatures, the limited
|
||||
decoded-CIL tuple fingerprint, constants, and bounded shape context. It
|
||||
reports all candidates at the winning score and remains ambiguous when the
|
||||
evidence does not distinguish them. The digest does not itself remap
|
||||
metadata tokens.
|
||||
For a matched method, an unavailable or partial body makes body-shape facets
|
||||
unknown while preserving observed signature differences. Structural identity
|
||||
compares normalized signatures, the limited decoded-CIL tuple fingerprint,
|
||||
constants, and bounded shape context. It reports all candidates at the winning
|
||||
score and remains ambiguous when the evidence does not distinguish them. The
|
||||
digest does not itself remap metadata tokens.
|
||||
|
||||
Tokens are always remapped through observed structure. A caller cannot carry
|
||||
`0x06001234` into a new MVID and assume it names the same method.
|
||||
|
||||
## Managed/native boundary rules
|
||||
|
||||
| Boundary | Managed observation | Native observation | Permitted link |
|
||||
| ----------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| P/Invoke | Module, entry point, charset/calling-convention flags, declaring method | Import/export/symbol/function evidence from the selected deep provider | Exact declared-name/module link or qualified resolution inference |
|
||||
| COM | Interop attributes, GUIDs, imported interfaces, method signatures | Native registration/vtable evidence when independently available | Identifier/signature inference with explicit environment limitations |
|
||||
| C++/CLI | Managed declaration and implementation flags | Native body/function evidence | Only a provider-supported bridge observation; never token-as-address |
|
||||
| ReadyToRun | Component/header and per-method CIL/native availability | Native section/function evidence | Authenticated image mapping with format/profile version |
|
||||
| Unmanaged export | Export metadata/attribute when present | PE export and native thunk/function | Exact export identity plus provider-qualified address |
|
||||
| Single-file host | Bundle entry/component identity | Host and native component evidence | Outer bundle plus component digest/extent commitment |
|
||||
| Unity IL2CPP | Supported metadata entity with authenticated pairing | Generated native function/type evidence | Versioned IL2CPP mapping only; no invented CIL |
|
||||
| Runtime-resolved native | API/constant/data-flow candidate | Loaded-module/symbol observation only under separate runtime execution authority | Static candidate remains inference until separately observed |
|
||||
| Boundary | Managed observation | Native observation | Permitted link |
|
||||
| ----------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| P/Invoke | Module, entry point, charset/calling-convention flags, declaring method | Import/export/symbol/function evidence from the selected deep provider | Exact declared-name/module link or qualified resolution inference |
|
||||
| COM | Interop attributes, GUIDs, imported interfaces, method signatures | Native registration/vtable evidence when independently available | Identifier/signature inference with explicit environment limitations |
|
||||
| C++/CLI | Managed declaration and implementation flags | Native body/function evidence | Only a provider-supported bridge observation; never token-as-address |
|
||||
| ReadyToRun | Component/header and per-method CIL/native availability | Native section/function evidence | Authenticated image mapping with format/profile version |
|
||||
| Unmanaged export | Export metadata/attribute when present | PE export and native thunk/function | Exact export identity plus provider-qualified address |
|
||||
| Single-file host | Bundle entry/component identity | Host and native component evidence | Outer bundle plus component digest/extent commitment |
|
||||
| Unity IL2CPP | Supported metadata entity with authenticated pairing | Generated native function/type evidence | Versioned IL2CPP mapping only; no invented CIL |
|
||||
| Runtime-resolved native | API/constant/data-flow candidate | Loaded-module/symbol observation from an independently collected runtime source | Static candidate remains inference until separately observed |
|
||||
|
||||
Declared import inventory supports a bounded positive claim. Its absence does
|
||||
not exclude dynamic resolution, generated code, protected code, native helpers,
|
||||
@@ -397,25 +405,13 @@ The benchmark may prove that REA reproduces selected facts for that exact local
|
||||
build. It cannot establish general support on its own; source-built conformance
|
||||
remains the admission requirement.
|
||||
|
||||
## Runtime admission boundary
|
||||
## Runtime behavior boundary
|
||||
|
||||
The later runtime track is not an extension flag on a static operation. Its
|
||||
design must expose whether it attaches to an existing process, launches a new
|
||||
process, loads an assembly, uses reflection, installs a debugger/profiler, or
|
||||
instruments code. Each effect needs separate approval and exact-build checks.
|
||||
|
||||
At minimum, admission requires target SHA-256, MVID, normalized signature and
|
||||
body/CIL commitment, CLR family, OS, architecture, tool version, scenario
|
||||
limits, output policy, and cleanup ownership to match. Any mismatch fails before
|
||||
the experiment. It must not contact real services/accounts or claim that an
|
||||
instrumented path represents an ordinary launch unless that proposition is
|
||||
independently tested.
|
||||
|
||||
The shipped `plan_managed_runtime_correlation` path admits only a
|
||||
default-disabled, permission-gated experiment plan and records that no target
|
||||
code was executed. Until a separate executor is designed and shipped, runtime
|
||||
behavior questions remain explicit unknowns with suggested probes; static
|
||||
support does not perform them.
|
||||
Managed inspection does not execute assemblies or observe CLR internals.
|
||||
Questions about actual behavior remain unknown until supported runtime
|
||||
evidence is collected. A direct process capture can retain declared inputs,
|
||||
outputs, filesystem and protocol activity for a target run; it does not prove
|
||||
which managed methods executed or how the CLR resolved them.
|
||||
|
||||
## Delivery sequence
|
||||
|
||||
@@ -437,9 +433,15 @@ The managed-code track advances as reviewable pull requests:
|
||||
PE/CLI corpus shipped through `npm run verify:managed`; optional BYO
|
||||
`ilspycmd` real-tool oracle shipped through `REA_ILSPY_CMD_PATH`; dnSpy and
|
||||
pinned Windows checks remain planned); and
|
||||
8. separately authorized runtime-correlation admission planning (shipped; no
|
||||
runtime execution);
|
||||
9. managed static Evidence projection into the application graph (shipped).
|
||||
8. managed static Evidence projection into the application graph (shipped).
|
||||
|
||||
Each implementation PR updates generated product facts only for behavior it
|
||||
actually ships and states which real-tool checks were performed.
|
||||
|
||||
### Method metadata during build comparison
|
||||
|
||||
Managed member comparisons report a `metadata` dimension when an observed
|
||||
MethodDef flags or implementation flags value differs, including accessibility
|
||||
or synchronization changes. This is separate from the CIL/signature matching
|
||||
tiers: identical instructions do not establish unchanged method metadata. The
|
||||
reported difference is static metadata evidence, not proof of runtime behavior.
|
||||
|
||||
+51
-62
@@ -8,16 +8,18 @@ registrations, reports their command vectors as aligned, stale, missing, or
|
||||
invalid, and keeps live-server state `unknown` unless the active connection
|
||||
supplies identity.
|
||||
|
||||
Canonical tool names remain stable, while `tools/list` advertises only the
|
||||
operations callable for the current target, provider, policy, host, and
|
||||
negotiated client capabilities. `binary_session.tool_availability` remains the
|
||||
complete inventory: it explains advertised and hidden operations with stable
|
||||
availability reasons and remediation. Each entry also reports required and
|
||||
optional negotiated client features plus the currently missing features. Form elicitation is optional for
|
||||
`capture_process_scenario`: an existing grant remains usable without it, while
|
||||
a missing grant cannot be elicited by a client that does not advertise it.
|
||||
Opening or closing a target, reloading policy, or observing a provider health
|
||||
transition emits `notifications/tools/list_changed`.
|
||||
`tools/list` returns the complete canonical tool inventory, including tools that
|
||||
are currently unavailable. Opening or closing a target or observing a provider
|
||||
health transition leaves that catalog unchanged and does not emit
|
||||
`notifications/tools/list_changed`.
|
||||
|
||||
Call `binary_session` with `{}` and read `result.tool_availability` to choose a
|
||||
callable operation for the current target, provider, host, and negotiated client
|
||||
capabilities. The default result includes the complete inventory with each
|
||||
tool's availability, reason, and remediation. Each entry also reports required
|
||||
and optional negotiated client features plus the currently missing features.
|
||||
The optional inputs `expected_package_version`, `expected_catalog_digest`, and
|
||||
`expected_server_path` compare the live session with the caller's expectations.
|
||||
|
||||
`binary_session.analysis_provider_candidates` is authoritative for deep-engine
|
||||
discovery. Target-free discovery is sorted by provider ID, reports host
|
||||
@@ -57,8 +59,13 @@ shutdown rules still apply; REA never kills a process it cannot prove it owns.
|
||||
|
||||
## Tool results
|
||||
|
||||
Evidence-producing tools return their result and Evidence context inline. Read
|
||||
that result directly. Use `get_evidence_bundle` when the task needs broader
|
||||
Evidence-producing tools return `{ result, evidence_id, evidence }` in both
|
||||
text and structured content. `evidence` is the complete canonical Evidence
|
||||
record, including `normalized_result`, which equals `result`. The same record
|
||||
is retained in the session bundle. Read `result` directly, or pass `evidence`
|
||||
to a compatible comparison tool: `analyze_function` Evidence can be passed
|
||||
directly to `compare_functions`, and `inspect_artifact` Evidence to
|
||||
`compare_artifacts`. Use `get_evidence_bundle` when the task needs broader
|
||||
retained session history or an explicit bundle for transfer.
|
||||
|
||||
## Aggregate native context
|
||||
@@ -74,65 +81,47 @@ unsupported facets are local `unavailable` outcomes. Use `current_document`,
|
||||
`current_address`, and `current_procedure` for direct single-field lookups; use
|
||||
the aggregate tools when you need related context together.
|
||||
|
||||
## Permission policy
|
||||
## Request scope and local effects
|
||||
|
||||
All local side effects use one scope vocabulary: capability, canonical roots,
|
||||
executables, environment variable names, network mode, mount permission, exact
|
||||
operation identity, and grant lifetime. Environment values and captured content
|
||||
are never part of a grant or denial.
|
||||
Each tool request names the target and lifecycle it will use. Browser calls
|
||||
carry a loopback CDP endpoint and target ID, with optional origin filters;
|
||||
process and Electron scenarios carry the executable, arguments, actions, and
|
||||
cleanup behavior; artifact tools carry the input path and requested operation.
|
||||
REA runs the declared request directly and does not infer a broader target or
|
||||
action from it.
|
||||
|
||||
Existing environment settings map to administrator ceilings. They remain the
|
||||
maximum authority:
|
||||
REA does not require permission grants or per-call approval flags. Setup still
|
||||
prints its plan and requires confirmation before changing configuration or
|
||||
installing Hopper. MCP clients control their own confirmation UI.
|
||||
|
||||
- process roots, executables, environment names, and external networking;
|
||||
- native mount enablement.
|
||||
Tool annotations describe effects and are hints, not authorization controls
|
||||
([MCP ToolAnnotations](https://modelcontextprotocol.io/specification/2025-06-18/schema#toolannotations)).
|
||||
`readOnlyHint` includes session state: an analysis call that records additive
|
||||
Evidence is marked non-read-only even when it leaves the target unchanged.
|
||||
`destructiveHint` describes possible data loss, not ordinary Evidence recording.
|
||||
Effect metadata covers possible behavior across supported inputs: DMG inventory
|
||||
can launch `hdiutil` and create an owned temporary mount directory; extraction
|
||||
creates a fresh output directory on every call. Comparing supplied web captures
|
||||
or PNG artifacts uses local data without contacting the browser.
|
||||
|
||||
Evidence bundles and analysis snapshots use the path supplied to the CLI or MCP
|
||||
tool; they do not require configured roots or separate read/write grants.
|
||||
Host requirements remain in force. macOS may deny Accessibility,
|
||||
Screen Recording, or native mounting; provider tools require their selected
|
||||
analysis runtime. These failures are reported at the operation that needs
|
||||
them. A configured provider or an endpoint alone does not establish that a
|
||||
target is supported.
|
||||
|
||||
Artifact extraction takes no path or occurrence selectors. It materializes all
|
||||
regular files into a fresh REA-chosen temporary directory. The write still
|
||||
passes through the active permission evaluator; the tool caller does not choose
|
||||
an output path.
|
||||
Evidence bundles, snapshots, and extraction use the paths and output behavior
|
||||
declared by their tools. Artifact extraction materializes the selected regular
|
||||
files into a fresh REA-chosen temporary directory and reports its path.
|
||||
|
||||
`rea policy status`, `list`, `explain`, and `revoke` inspect the same evaluator
|
||||
used by MCP. Optional project grants require both
|
||||
`REA_PERMISSION_PROJECT_ROOT` and `REA_PERMISSION_PROJECT_STORE`. The store is
|
||||
atomically written with mode `0600`, bound to the canonical project root, and is
|
||||
never enabled by default. Send `SIGHUP` to the REA MCP process after a trusted
|
||||
project-store change; project grants reload without restarting. Environment
|
||||
settings belong to the process environment, so changing an administrator
|
||||
ceiling in an MCP registration requires restarting that registered server or
|
||||
its owning client.
|
||||
Revocation affects future operations; an already-running operation retains the
|
||||
decision made at its preflight boundary.
|
||||
Once and session grants are connection-local overlays. They cannot authorize a
|
||||
second MCP connection, be consumed by another connection, or be cleared when a
|
||||
different connection closes. Live connections evaluate those overlays against
|
||||
the current process-wide ceilings and persisted grants, including successful
|
||||
SIGHUP reloads.
|
||||
`rea policy revoke <grant-id>` displays the exact grant and requires interactive
|
||||
confirmation; automation must pass `--yes` (or `-y`) explicitly.
|
||||
|
||||
Denials use the shared `permission_required` schema with requested scope, missing
|
||||
scope, administrator ceiling, elicitation support, and exact restart status.
|
||||
Client-provided roots are context only and never grants.
|
||||
|
||||
Elicitation can add a once or session grant only inside an existing
|
||||
administrator ceiling. A request outside that ceiling reports
|
||||
`elicitation_supported: false` and `restart_required: true`; interactive consent
|
||||
cannot silently widen administrator policy.
|
||||
|
||||
`analyze_javascript_application` reads the supplied local directory or ASAR path directly and returns its result and Evidence inline; no approval flag or root configuration is needed.
|
||||
`analyze_javascript_application` reads the supplied local directory or ASAR path
|
||||
and returns its result and Evidence inline.
|
||||
|
||||
## Integrity record-and-continue
|
||||
|
||||
Artifact integrity remains fail-closed by default. Record-and-continue requires
|
||||
all three conditions:
|
||||
|
||||
1. operator policy `REA_ARTIFACT_INTEGRITY_CONTINUE_ENABLED=true`;
|
||||
2. `integrity_policy=record-and-continue`;
|
||||
3. explicit per-call `integrity_continue_approved=true`.
|
||||
Artifact integrity fails closed by default. A request can explicitly select
|
||||
`integrity_policy=record-and-continue` when the investigation needs verified
|
||||
siblings to continue after a mismatch.
|
||||
|
||||
Contradictory bytes are quarantined from nested expansion and recorded with
|
||||
declared and observed hashes, trust, provenance, path, and unpacked state.
|
||||
|
||||
+4
-6
@@ -13,7 +13,7 @@ invoke tools by themselves.
|
||||
| `verify_reconstruction` | Evaluate a finite reconstruction specification against retained comparison Evidence. |
|
||||
| `trace_crash` | Correlate a crash symptom with static paths and optional process capture evidence. |
|
||||
| `audit_residual_unknowns` | Audit current residual-unknown heads and evidence-qualified resolution. |
|
||||
| `prepare_bounded_process_capture` | Design an approval-gated controlled process experiment before execution. |
|
||||
| `prepare_bounded_process_capture` | Design a bounded controlled process experiment before execution. |
|
||||
|
||||
Every rendered prompt provides optional starting points using current REA tool
|
||||
names. Agents can call tools directly, skip irrelevant suggestions, and inspect
|
||||
@@ -108,8 +108,6 @@ session are no longer suggested. Provider errors, unsupported operations, and
|
||||
malformed provider output produce an empty completion list rather than an
|
||||
unverified identifier.
|
||||
|
||||
Suggestions never grant permission. In particular, selecting an artifact
|
||||
occurrence does not authorize extraction, selecting an unknown does not
|
||||
authorize mutation, and selecting a prior capture does not authorize process
|
||||
execution. The corresponding tools retain their existing approval, policy,
|
||||
effect, and validation boundaries.
|
||||
Suggestions are starting points, not tool calls. A tool acts on the target,
|
||||
operation, and lifecycle fields in its own request; selecting a suggestion does
|
||||
not execute it or broaden those fields.
|
||||
|
||||
+162
-67
@@ -1,78 +1,173 @@
|
||||
# Native UI and dispatch investigation
|
||||
|
||||
REA combines decoded application resources, provider metadata, and native code
|
||||
analysis into evidence-linked investigation graphs. Each layer has a different
|
||||
coverage boundary; a graph edge is not automatically proof that a runtime path
|
||||
executes.
|
||||
REA joins static resources, native metadata and code facts while keeping runtime
|
||||
observations separate. Every result reports its evidence, target identity,
|
||||
coverage and unknowns. The CLI and MCP use the same application workflows.
|
||||
|
||||
## Current workflow
|
||||
## Static inspection
|
||||
|
||||
1. `decode_interface_builder` reads compiled Apple storyboard and nib resources
|
||||
from an artifact graph and returns scenes, objects, outlets, actions,
|
||||
connections, and available controller/class names. A source-owned AppKit
|
||||
XIB is compiled by `ibtool` in `verify:interface-builder` and checked for
|
||||
its hierarchy and target/action edge. Storyboard compilation still depends
|
||||
on an installed iOS platform; unsupported archive objects remain explicit.
|
||||
2. `inspect_native_dispatch_metadata` normalizes supported provider name
|
||||
inventory into typed Objective-C and Swift records. Current extraction is
|
||||
symbol-name based: symbolized Objective-C method implementations and
|
||||
mangled Swift names can be reported as partial evidence. It does not yet
|
||||
decode Objective-C runtime sections, ivar layouts, protocol conformance
|
||||
records, Swift witness tables, vtables, relative pointers, or closure
|
||||
thunks from binary metadata.
|
||||
3. `trace_native_ui_action` accepts one selector or Interface Builder object
|
||||
ID. REA decodes the active app and reads names from the bound native provider,
|
||||
verifies that both observations refer to the same target digest, joins an
|
||||
authored action to a symbolized Objective-C implementation, and follows
|
||||
bounded direct callees. Ambiguous selectors and unresolved calls remain
|
||||
explicit. This is a static candidate route, not proof of runtime reachability;
|
||||
it does not resolve Swift witnesses, vtables, closures, or cross-function
|
||||
value flow yet. The CLI uses the same query: `rea trace-native-ui-action
|
||||
<app-or-binary> <selector-or-object-id>`.
|
||||
`inspect_native_dispatch_metadata` and `rea inspect-native-dispatch-metadata`
|
||||
expose the provider's typed symbol-derived inventory separately.
|
||||
4. Ghidra `analyze_function` returns a bounded high-p-code def-use view for the
|
||||
selected function. This can support manual state/value analysis, but REA
|
||||
does not yet compose those views into an automatic cross-function slice.
|
||||
- `inspect_asset_catalog` / `rea inspect-asset-catalog <app>` reads compiled
|
||||
`Assets.car` metadata through macOS `assetutil --info`. Catalog digests, raw
|
||||
rendition fields, pagination and exact UI resource-name matches are returned.
|
||||
Image extraction and undocumented rendition interpretation are unsupported.
|
||||
- `inspect_keyed_archive` / `rea inspect-keyed-archive <plist>` reads XML or
|
||||
binary Foundation keyed archives without instantiating classes. For an active
|
||||
app, provide its relative archive path as the second CLI argument or MCP
|
||||
`path`. Results retain original `$objects` indices, named `$top` roots,
|
||||
shared/cyclic references, class descriptors, raw serialized fields and
|
||||
malformed/unresolved/nil references. Missing fields stay absent; unknown
|
||||
classes are preserved. Select `root`, `offset` and `limit` explicitly.
|
||||
UID 0 cannot distinguish conditional nil from ordinary nil. Limits are
|
||||
64 MiB of input, 200,000 objects/references and 128 nesting levels.
|
||||
- `decode_interface_builder` / `rea decode-interface-builder <app>` reads
|
||||
compiled storyboard and nib resources into objects, outlets, actions,
|
||||
connections and controller/class names. `verify:interface-builder` compiles
|
||||
a source-owned AppKit XIB with `ibtool`. Storyboards require an installed
|
||||
iOS platform; unsupported archive forms remain explicit.
|
||||
- `inspect_native_dispatch_metadata` / `rea inspect-native-dispatch-metadata
|
||||
<app-or-binary>` prefers a validated macOS Mach-O byte reader. It decodes
|
||||
64-bit little-endian Objective-C class/metaclass records, superclass pointers,
|
||||
absolute/relative method entries, ivar offsets/sizes/alignment and protocol
|
||||
declarations. It also decodes simple Swift conformances, static synchronous
|
||||
witness slots, signed-relative pointers and non-generic, non-resilient class
|
||||
vtable descriptors. Records include exact virtual addresses/file offsets and
|
||||
artifact evidence. Vtable indexes are metadata word offsets; witness indexes
|
||||
start after the conformance header. Names unavailable in metadata remain null.
|
||||
Chained fixups, external bindings, authenticated pointers, categories,
|
||||
properties, generic/resilient/async/coroutine tables and inherited overrides
|
||||
have explicit unsupported or partial coverage. Other providers retain the
|
||||
existing symbol-based inventory with its narrower coverage.
|
||||
|
||||
## Ghidra p-code result boundary
|
||||
## Native instruction, call and type primitives
|
||||
|
||||
The dossier retains up to 3,000 p-code operations, up to 64 inputs per
|
||||
operation, and 12,000 def-use edges. Each operation carries its address,
|
||||
sequence, opcode, input/output varnodes, and whether Ghidra marks it dead.
|
||||
Memory reads/writes, conditional and indirect branches, and direct/indirect
|
||||
calls are reported by opcode with operand indexes. Results expose truncation,
|
||||
an omitted-operation lower bound, and known omitted-input and omitted-edge
|
||||
counts.
|
||||
Ghidra supplies three exact-object operations:
|
||||
|
||||
These are decompiler-derived static observations. Def-use edges stop at a
|
||||
function boundary; memory aliasing, call side effects, unresolved dispatch,
|
||||
runtime behavior, and application-level meaning require additional evidence.
|
||||
A p-code `STORE` can describe stack memory and does not by itself identify
|
||||
persistent application state.
|
||||
```bash
|
||||
rea inspect-native-instruction <binary> <address> --provider ghidra
|
||||
rea resolve-native-call-targets <binary> <call-site> --provider ghidra
|
||||
rea inspect-native-data-type <binary> --type /MyStruct --provider ghidra
|
||||
rea inspect-native-data-type <binary> --address <typed-data-address> --provider ghidra
|
||||
```
|
||||
|
||||
Recovered jump-table load sources and case-to-target mappings are reported as
|
||||
separate facts. On AArch64, REA additionally decodes the verified byte-indexed
|
||||
relative branch-table form when the selector's unsigned upper bound, `LDRB`
|
||||
table load, `ADR` branch base, scaled `ADD`, indirect `BR`, and recovered
|
||||
target set agree. It reads only the proven entry count and reports one direct
|
||||
mapping per selector value. Other layouts retain unknown case values and
|
||||
unknown table bounds when Ghidra does not provide aligned labels or load-table
|
||||
metadata; REA does not extrapolate from neighboring targets.
|
||||
Instruction facts include bytes, length, decoder text, ordered register/scalar/
|
||||
address tokens, typed references and flow destinations. Mid-instruction, data,
|
||||
outside-memory and undecodable addresses have separate outcomes. Effective
|
||||
memory base/index/displacement roles and per-instruction context mode remain
|
||||
unavailable; `mode` is the program language variant.
|
||||
|
||||
## Provider admission and remaining work
|
||||
Call resolution reports direct, resolved indirect, ambiguous, unresolved and
|
||||
non-call outcomes from static call references. It does not establish runtime
|
||||
execution or classify Objective-C/Swift/vtable/closure mechanisms from names.
|
||||
|
||||
Ghidra is admitted on Linux x64; the experimental Windows x64 P0 is limited to
|
||||
approved native PE applications. macOS is not an admitted Ghidra host. The
|
||||
optimized AArch64 ELF byte-table fixture has a separate lane so its acceptance
|
||||
does not depend on PE or Mach-O cross-linkers.
|
||||
Type inspection selects one exact database pathname or typed data address.
|
||||
Struct/union fields, enums, pointers, arrays, typedefs, size, alignment, packing
|
||||
and bitfields retain database authority. Child types use exact IDs for further
|
||||
inspection, including recursive layouts. Source/debug authority and flexible
|
||||
array semantics are not inferred. The real lane uses a native DWARF 4 object
|
||||
with a struct containing a union, enum and recursive pointer; a stripped linked
|
||||
binary may have no corresponding recovered layout.
|
||||
|
||||
The native feature set is still partial. Objective-C/Swift metadata currently
|
||||
uses symbol names rather than runtime sections, witness tables, vtables, or
|
||||
relative pointers. UI action traces do not yet resolve Swift dispatch or
|
||||
closure/thunk routes. Value dependencies remain intra-function, and the
|
||||
AArch64 switch lane currently proves one optimized byte-indexed ELF layout;
|
||||
sparse, stripped, Mach-O, and other table patterns remain unknown. The local
|
||||
Xcode installation has no iOS simulator platform, so compiled storyboard
|
||||
acceptance could not run here. These boundaries should stay visible until
|
||||
matching provider-backed readers and fixtures exist.
|
||||
## Bounded static traces
|
||||
|
||||
`trace_native_ui_action` / `rea trace-native-ui-action <target> <seed>` accepts
|
||||
one authored selector/object ID, native symbol or exact function address. It
|
||||
joins UI wiring to uniquely matched class/selector implementations and follows
|
||||
bounded typed call references. Direct references, resolved indirect references,
|
||||
ambiguous candidates, inferred untyped provider callees and targetless call
|
||||
sites remain distinct. A static route does not establish runtime reachability.
|
||||
Swift/closure dispatch requires resolved static references; unavailable ABI
|
||||
forms remain unknown.
|
||||
|
||||
`trace_native_values` / `rea trace-native-values <binary> <procedure> --provider
|
||||
ghidra` composes high-p-code def-use graphs across statically resolved calls.
|
||||
Logical CALL arguments bind to recovered parameter ordinals; recovered RETURN
|
||||
values bind to CALL outputs. These edges are decompiler-derived. Constants,
|
||||
operators, LOAD/STORE and branch operands are included inline. Alias effects,
|
||||
persistent state, RNG roles, missing/variadic bindings and ambiguous destinations
|
||||
remain unknown. Budgets control depth, decompilations, call-site resolutions,
|
||||
nodes and edges. Node payloads are bounded to 8 MiB and serialized results to
|
||||
32 MiB. Pagination returns edges whose source is on the node page and preserves
|
||||
stable IDs for endpoints outside that page.
|
||||
|
||||
The raw Java `is_dead` flag can remain set after Ghidra decodes a live block;
|
||||
`block_membership` reports actual syntax-tree membership separately.
|
||||
|
||||
The provider dossier itself retains up to 3,000 p-code operations, 64 inputs per
|
||||
operation and 12,000 def-use edges, with explicit omitted counts. A `STORE`
|
||||
may describe stack memory and does not itself prove persistent state.
|
||||
|
||||
Jump-table evidence keeps numeric `mappings`, explicit `default_targets`, and
|
||||
backing `data_sources` separate. A null case value denotes an unresolved
|
||||
case, never a known default. Ghidra pairs typed case/default tokens with the
|
||||
recovered block entry and its unique indirect dispatch predecessor; it does
|
||||
not zip unequal label and destination arrays. Shared case bodies retain each
|
||||
label. Negative numeric tokens are checked against their encoded magnitude;
|
||||
nonliteral, ambiguous, or nonexact JSON integer labels remain unknown.
|
||||
Unknown-label diagnostics retain the token text, encoded unsigned magnitude,
|
||||
target and dispatch so callers can inspect the original observations.
|
||||
Decompiler load-table metadata supplies observed entry sizes and counts
|
||||
without assigning every backing table to each mapping. These observations
|
||||
describe the decompiler's recovered switch, not guaranteed original source.
|
||||
Legacy records without `default_targets` normalize to an empty array and
|
||||
retain their existing unresolved mappings.
|
||||
|
||||
AArch64 jump-table recovery additionally verifies byte and halfword relative
|
||||
forms from unsigned bounds, register definitions, table loads, branch bases,
|
||||
scaled ADD/BR instructions and the recovered target set. It reads exactly the
|
||||
proven count and preserves unknowns for other forms. Real ELF and host ARM64
|
||||
Mach-O fixtures check each case against source-owned return values.
|
||||
|
||||
## Native desktop observation
|
||||
|
||||
`observe_native_ui` captures one explicitly selected existing PID/window ID.
|
||||
Screenshots use a selected-window ScreenCaptureKit
|
||||
filter on macOS 14+; accessibility reads stay within that window. Missing Screen
|
||||
Recording/Accessibility permissions produce actionable errors without broad
|
||||
capture or automatic permission prompts. Executable bytes and process launch
|
||||
time guard against a different target or PID reuse. AX selection requires one
|
||||
unique geometry match; ambiguity fails closed.
|
||||
|
||||
`capture_native_ui_scenario` takes AX child-index paths for press, increment/
|
||||
decrement scrolling and text-value entry, or bounded waits. No global event
|
||||
injection is used. Unsupported AX actions fail explicitly. The result preserves
|
||||
ordered before/after captures and gaps; an action may have occurred before a
|
||||
post-action capture fails. Application state is left as-is; REA does not attempt
|
||||
to restore it.
|
||||
|
||||
Scenarios accept caller-selected action lists and accessibility node counts.
|
||||
Individual waits cannot exceed the operation's 180-second deadline, and the
|
||||
complete scenario result has a 64 MiB output budget. Screenshots are scaled to
|
||||
at most 2,048 pixels and captured only for the selected window. REA compiles one
|
||||
owned helper per scenario, removes its temporary compiler cache and stops its
|
||||
helper on cancellation. It does not launch or own the selected application. UI
|
||||
actions may change application data or trigger network activity.
|
||||
|
||||
## Provider and verification boundaries
|
||||
|
||||
Install Ghidra 12.1.4 and a full 64-bit JDK 21 separately, then configure REA to
|
||||
use them. Ghidra analysis supports Linux x64 and macOS x64/arm64; macOS requires
|
||||
the matching native decompiler. Experimental Windows x64 P0 admits native
|
||||
x86-64 PE applications on local NTFS using bundled Job Object ownership,
|
||||
protected runtime DACLs, and handle-based path admission. See [Windows Ghidra P0](windows-ghidra-p0.md) and
|
||||
[issue #527](https://github.com/morluto/rea/issues/527).
|
||||
On Linux and macOS, `annotate_native_function` atomically edits a function name
|
||||
and entry comments in the ephemeral database, returning refreshed analysis
|
||||
without changing executable bytes. Windows P0 remains read-only. Ghidra has no
|
||||
GUI authority, and REA never falls back automatically to Hopper.
|
||||
|
||||
- `npm run verify:ghidra`: host-native debug/stripped targets, native type layout,
|
||||
instruction/call facts, value dependencies and process/project cleanup.
|
||||
- `npm run verify:ghidra:aarch64-jump-table`: optimized ELF and byte/halfword
|
||||
relative tables, plus ARM64 Mach-O on an ARM64 macOS host.
|
||||
- `npm run verify:apple-dispatch`: source-built Objective-C protocols/classes and
|
||||
Swift conformances/vtables, repeated after stripping local symbols.
|
||||
- `npm run verify:native-ui`: one source-owned fixture window, successful
|
||||
selected-window capture and actions, changed-target rejection, and cleanup.
|
||||
Missing OS permissions fail this lane.
|
||||
- `npm run verify:native-ui:permissions`: permits a permission-denial result and
|
||||
reports `positive_e2e: false` when capture is denied. That result verifies the
|
||||
OS permission boundary, not successful UI capture or actions.
|
||||
|
||||
macOS ARM64 is the real host verified during this implementation. Admission of
|
||||
macOS Intel does not claim an Intel verification run. Unsupported metadata and
|
||||
unresolved runtime/value semantics remain visible in results.
|
||||
|
||||
+64
-243
@@ -1,79 +1,34 @@
|
||||
# Process capture
|
||||
|
||||
Process Capture records one approved command as deterministic Evidence. It is
|
||||
intended for authority-versus-reconstruction checks where terminal behavior,
|
||||
child lifetime, filesystem state, or dependency timing matters.
|
||||
Process Capture runs one caller-selected command and records bounded evidence
|
||||
about its terminal behavior, scheduled interactions, process lifetime, and
|
||||
selected filesystem state. Use it to compare two direct runs of the same
|
||||
scenario, such as an authority build and a reconstruction. It does not replay a
|
||||
previous execution or emulate dependencies.
|
||||
|
||||
The harness records both raw PTY chunks and terminal states rendered by xterm.
|
||||
It also records scripted input, resize and signal delivery, sampled process
|
||||
metadata, named filesystem checkpoints, command-shim invocations, loopback
|
||||
HTTP/WebSocket exchanges, and process exit ownership. Missing or bounded
|
||||
observations remain explicit; a truncated capture is never treated as
|
||||
equivalent to another capture.
|
||||
The command runs with the current user's permissions and inherits the host
|
||||
environment before applying the scenario's explicit overrides. This is not a
|
||||
sandbox. Filesystem observation paths select what REA snapshots; they do not
|
||||
restrict what the process can read or write. The inherited environment is not
|
||||
recorded, so its influence may remain unknown.
|
||||
|
||||
Every capture includes a run manifest with canonical SHA-256 commitments for
|
||||
the secret-safe full scenario projection, comparison contract, executable,
|
||||
normalization rules, command-shim plan, and replay plan. The manifest also
|
||||
records the REA/provider versions, platform, architecture, PTY backend, and UTC
|
||||
start/completion timestamps. Capture creation, Evidence import, and comparison
|
||||
recompute the self-contained commitments and reject invalid lifecycle data.
|
||||
## Capture a command
|
||||
|
||||
## Enable the capability
|
||||
Write a JSON scenario and pass its path to the CLI:
|
||||
|
||||
Process execution is disabled until the operator supplies policy roots and
|
||||
approves host networking:
|
||||
|
||||
```bash
|
||||
export REA_PROCESS_CAPTURE_ENABLED=true
|
||||
export REA_PROCESS_EXECUTABLE_ROOTS_JSON='["/usr/bin","/absolute/project/bin"]'
|
||||
export REA_PROCESS_WORKING_ROOTS_JSON='["/absolute/project"]'
|
||||
export REA_PROCESS_ALLOWED_ENV_JSON='["PATH"]'
|
||||
export REA_PROCESS_ALLOW_EXTERNAL_NETWORK=true
|
||||
```
|
||||
|
||||
By default, enabling process capture also installs an administrator-lifetime
|
||||
grant for the configured boundary. Set
|
||||
`REA_PROCESS_CAPTURE_AUTO_GRANT=false` to install the same boundary only as an
|
||||
administrator ceiling. Captures then fail with a structured permission-required
|
||||
result until a narrower grant is established; this is the prerequisite mode for
|
||||
connection-scoped MCP elicitation. It does not itself authorize a capture.
|
||||
Each MCP connection owns its elicited once and session grants. A grant accepted
|
||||
on one connection is not visible to another connection, and disconnect cleanup
|
||||
removes only the grants owned by the connection that closed. Reloaded
|
||||
administrator ceilings and persisted grants still apply immediately to every
|
||||
live connection.
|
||||
|
||||
REA offers the once-or-session grant form only after the connection negotiates
|
||||
an MCP revision with multi-round tool results and advertises form elicitation.
|
||||
Older connections keep returning the structured
|
||||
permission-required result and never wait for an unsupported elicitation
|
||||
response. On the pinned `@modelcontextprotocol/server` 2.0.0 runtime, this is
|
||||
the official `2026-07-28` `inputRequired` continuation: the SDK seals the
|
||||
request state and validates the continuation envelope, while REA validates the
|
||||
untrusted input response before creating a connection-owned grant. The older
|
||||
push-style `elicitation/create` path is not used for this protocol revision.
|
||||
|
||||
Executables, working directories, filesystem roots, and requested environment
|
||||
variables are checked against operator policy before launch. Process Capture is an observation tool,
|
||||
not a security sandbox: the target runs with the current user's permissions.
|
||||
|
||||
## Capture a scenario
|
||||
|
||||
`rea capture-process` reads a JSON scenario and writes process capture
|
||||
Evidence to stdout:
|
||||
|
||||
```bash
|
||||
```sh
|
||||
rea capture-process ./scenario.json > capture.json
|
||||
```
|
||||
|
||||
A representative scenario is:
|
||||
For example:
|
||||
|
||||
```json
|
||||
{
|
||||
"executable": "/absolute/path/to/node",
|
||||
"executable": "node",
|
||||
"arguments": ["./signup.mjs"],
|
||||
"working_directory": "/absolute/project",
|
||||
"filesystem_roots": ["/absolute/project/state"],
|
||||
"working_directory": ".",
|
||||
"environment": { "APP_MODE": "test" },
|
||||
"filesystem_observation_paths": ["./state"],
|
||||
"terminal": { "columns": 80, "rows": 24, "scrollback": 1000 },
|
||||
"events": [
|
||||
{
|
||||
@@ -82,197 +37,63 @@ A representative scenario is:
|
||||
"data": "user@example.test\r",
|
||||
"sensitive": true
|
||||
},
|
||||
{ "type": "resize", "at_ms": 500, "columns": 100, "rows": 30 }
|
||||
{ "type": "resize", "at_ms": 500, "columns": 100, "rows": 30 },
|
||||
{ "type": "signal", "at_ms": 1000, "signal": "SIGINT" }
|
||||
],
|
||||
"checkpoints": [
|
||||
{
|
||||
"name": "signup_ready",
|
||||
"trigger": { "type": "terminal_literal", "value": "All set!" }
|
||||
},
|
||||
{ "name": "root_exited", "trigger": { "type": "root_exit" } }
|
||||
],
|
||||
"command_shims": [
|
||||
{
|
||||
"name": "codex",
|
||||
"routes": [
|
||||
{
|
||||
"arguments": ["--version"],
|
||||
"outputs": [
|
||||
{ "at_ms": 0, "stream": "stdout", "data": "codex 1.2.3\n" }
|
||||
],
|
||||
"termination": { "type": "exit", "code": 0 },
|
||||
"max_calls": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
"timeout_ms": 30000,
|
||||
"idle_timeout_ms": 30000,
|
||||
"settle_ms": 100
|
||||
}
|
||||
```
|
||||
|
||||
Input events marked `sensitive` are dispatched to the PTY but stored only as a
|
||||
byte-count placeholder. Environment values listed in `secret_aliases` are also
|
||||
redacted from recorded text. The command-shim directory is placed first in the
|
||||
captured process `PATH`; any remaining `PATH` must be explicitly supplied or
|
||||
inherited under operator policy.
|
||||
`executable` is required. `arguments`, `working_directory`, `environment`,
|
||||
`filesystem_observation_paths`, terminal settings, timed `events`, timeouts,
|
||||
resource limits, and normalization settings have defaults; see
|
||||
`processScenarioSchema` for the exact contract. Event times are milliseconds
|
||||
from launch, must be ordered, and must fall within `timeout_ms`. Inputs marked
|
||||
`sensitive` are sent to the process but persisted as a byte-count placeholder.
|
||||
Environment overrides are recorded in the scenario commitment; inherited
|
||||
values are not copied into Evidence.
|
||||
|
||||
Scenario arguments, environment names, timed interactions, shim routes and
|
||||
output chunks, filesystem checkpoints, and static HTTP/WebSocket scripts are
|
||||
accepted without fixed item-count ceilings. The run still obeys its timeout
|
||||
and the configured output-byte, process, filesystem, protocol-event, body-byte,
|
||||
and connection budgets; each filesystem checkpoint uses those same
|
||||
bounded snapshot limits.
|
||||
The capture contains raw PTY output chunks and rendered terminal states,
|
||||
interaction dispatch outcomes, exit reason, sampled process-tree observations,
|
||||
and settlement status. When filesystem paths are selected, REA records
|
||||
`files_before` and `files_after`, then classifies observed entries as created,
|
||||
deleted, modified, or unchanged. These are bounded snapshots, not a syscall
|
||||
trace; short-lived changes between snapshots may be missed. With no selected
|
||||
paths, filesystem effects remain unknown.
|
||||
|
||||
## Reactive process scenarios
|
||||
Output, file count, file size, process sampling, filesystem depth, total
|
||||
runtime, idle time, and post-exit settlement are bounded by the scenario's
|
||||
limits. The result marks truncated observations and residual unknowns rather
|
||||
than treating missing data as proof of equivalence. Cancellation and timeout
|
||||
run the same owned-process cleanup path. Settlement reports whether the
|
||||
sampled process group quiesced or whether cleanup was needed or unverifiable;
|
||||
sampling cannot prove that every short-lived or detached descendant was seen.
|
||||
|
||||
Set `reactive` to a process reactive scenario declaration when interaction
|
||||
must follow observed output instead of guessed delays. The graph declares an
|
||||
initial state, absolute scenario and state deadlines, explicitly prioritized
|
||||
transitions, caller-chosen use and visit counts, trigger predicates, ordered
|
||||
actions, and either a next state or `passed` outcome.
|
||||
## Compare two captures
|
||||
|
||||
The admitted runtime slice matches decoded UTF-8 terminal literals and exact
|
||||
`terminal_raw`, `interaction`, `process`, `filesystem`, `http`, `websocket`, or
|
||||
`shim` journal events. Process events are sampled observations, not an
|
||||
event-complete lifecycle. Filesystem events identify named checkpoints; shim
|
||||
events identify a command and route. Live predicates receive the same normalized
|
||||
and redacted payload later persisted in Evidence. Predicates may compose with
|
||||
`all`, `any`, `sequence`, and `repeat` nodes and may select a frontier at
|
||||
scenario start, state entry, a prior checkpoint, or an exact event ID. Actions
|
||||
can write PTY input, resize it, signal the root process, or capture a named
|
||||
filesystem checkpoint. `terminal_rendered`, `lifecycle`, and `replay_transition`
|
||||
event triggers and non-root signal selectors fail during preflight before the
|
||||
target launches.
|
||||
Compare saved capture Evidence with:
|
||||
|
||||
Reactive results are recorded in `reactive_run`, including the terminal outcome,
|
||||
active state, matched event IDs, action evidence IDs, and deterministic
|
||||
transition journal. The reducer retains and evaluates every admitted
|
||||
observation through the scenario deadline; it has no fixed state, transition,
|
||||
predicate, action, repeat, or history-count ceilings. Recursive input depth has
|
||||
a stack safety guard. Terminal controls also commit the last admitted
|
||||
observation order, so later output cannot be moved ahead of a timeout,
|
||||
cancellation, or target loss during validation. If the process exits before the graph finishes,
|
||||
the outcome is `target_lost`; state/scenario deadlines remain distinct
|
||||
`predicate_timeout`/`scenario_deadline` outcomes. Timed `events` remain
|
||||
available for fixed schedules, but they are not silently translated into the
|
||||
reactive graph.
|
||||
|
||||
## Checkpoints and deterministic shims
|
||||
|
||||
Checkpoint triggers may use an elapsed time, a terminal literal and occurrence
|
||||
count, root exit, or settlement. REA always includes `before` and
|
||||
`after_settlement` snapshots. Snapshot effects are relative to the preceding
|
||||
checkpoint, so transient files remain visible even when the final tree matches
|
||||
the initial tree.
|
||||
|
||||
Each command shim matches an exact argument array. Routes can emit timed stdout
|
||||
or stderr chunks and then exit or receive `SIGINT`, `SIGTERM`, or `SIGKILL`.
|
||||
Unmatched and exhausted calls are recorded. Shim observations are included in
|
||||
the capture without a separate fixed count ceiling. Other configured
|
||||
observation and byte budgets remain explicit in the capture result when they
|
||||
affect completeness.
|
||||
|
||||
## Run a replay machine directly
|
||||
|
||||
Use `rea run-replay-machine ./run.json` to validate a finite replay machine
|
||||
against an ordered `events` array without launching a process or opening a
|
||||
socket. MCP clients use `run_replay_machine` with the same `{ machine, events }`
|
||||
input. The result retains one decision per offered event, a transition journal
|
||||
with capture alias metadata, and one redacted action-table entry per used
|
||||
transition. It never returns request bodies, request headers, or captured
|
||||
values. It also reports the initial and final states, whether the final state is
|
||||
terminal, configured limits, and committed usage.
|
||||
|
||||
The direct evaluator has no fixed state, transition, event, guard, capture,
|
||||
action, or action-byte count ceiling. It indexes transitions by protocol and
|
||||
path, evaluates events in order, and returns a decision for each offered event
|
||||
plus actions for used transitions. Caller-declared `max_uses`, `max_visits`,
|
||||
`max_transitions`, and protocol budgets determine the finite run; there is no
|
||||
upper schema cap on those values. Result size follows the input and is not
|
||||
truncated by an arbitrary item quota.
|
||||
|
||||
Refused events remain data rather than aborting the run. Outcomes distinguish
|
||||
unmatched events, invalid states, failed guards, exhausted transitions, invalid
|
||||
captures, unexpected reconnects, and exhausted limits. This direct runner
|
||||
evaluates the same domain runtime used by loopback Process Capture, but performs
|
||||
no network or target execution.
|
||||
|
||||
## Compare captures
|
||||
|
||||
Compare two saved Evidence records with:
|
||||
|
||||
```bash
|
||||
```sh
|
||||
rea compare-process-captures authority.json reconstruction.json
|
||||
```
|
||||
|
||||
Pass an optional third path, `rea compare-process-captures authority.json
|
||||
reconstruction.json trace.json`, when valid executions may differ in declared
|
||||
concurrent ordering. MCP callers pass the same object as `trace_spec` to
|
||||
`compare_process_captures`. A trace specification declares exact named events
|
||||
and chooses one language:
|
||||
The comparison checks terminal, interaction, exit, filesystem, and
|
||||
process observations under a shared comparison contract. It reports observed
|
||||
differences with their locations, while residual unknowns or truncated
|
||||
observations prevent a complete-equivalence claim. Matching scenario
|
||||
commitments do not reveal whether redacted sensitive inputs were equal.
|
||||
|
||||
- `partial_order` requires every event pair to be related by a
|
||||
`happens_before` edge (including transitive edges) or an explicit
|
||||
`unordered_groups` declaration. `not_before` expresses negative ordering
|
||||
constraints without inferring causality. Optional `prefix` and `suffix`
|
||||
arrays remain exact.
|
||||
- `finite_traces` lists the complete accepted variants. An `unordered` token
|
||||
accepts permutations of exactly its declared event multiset; it does not
|
||||
drop or broadly sort events.
|
||||
For scenarios where independent scheduling can change event order, the CLI
|
||||
also accepts an optional trace-specification JSON file as the third argument.
|
||||
That specification must state the exact events and ordering constraints to
|
||||
accept; it does not discard unmatched observations or make timestamps into
|
||||
causal evidence. Trace comparison returns `unknown` when a capture lacks the
|
||||
required complete event journal or contains relevant residual unknowns.
|
||||
|
||||
Each event uses one source family (`terminal_raw`, `terminal_rendered`,
|
||||
`interaction`, `lifecycle`, `process`, `filesystem`, `http`, `websocket`,
|
||||
`shim`, or `replay_transition`), an exact JSON payload, and a discriminated
|
||||
cardinality: `required`, `optional`, `exact`, or `range`. Specifications with
|
||||
duplicate or overlapping predicates, unknown references, cycles, implicit
|
||||
ordering gaps, ordered/unordered conflicts, or finite variants that violate
|
||||
cardinality are rejected before comparison.
|
||||
|
||||
Event payloads are exact by default. To compare schedules whose recorder
|
||||
metadata necessarily changes, an event may explicitly list top-level
|
||||
`ignore_fields` from the bounded set `sequence`, `at_ms`, `scheduled_at_ms`,
|
||||
`dispatched_at_ms`, and `elapsed_ms`; its `exact` object must omit those fields.
|
||||
All predicates for one source use the same ignore set, so relaxed metadata
|
||||
cannot create declaration-order matching ambiguity. Other payload fields are
|
||||
never dropped or normalized by trace comparison.
|
||||
|
||||
Process Capture records a monotonic `event_journal` at observation time. Trace
|
||||
comparison uses only this capture order for cross-source causality; timestamps
|
||||
are retained as event data but never promoted into happens-before evidence.
|
||||
Older captures without a complete journal, truncated captures, and relevant
|
||||
residual unknowns return a trace verdict of `unknown`. A passing result retains
|
||||
each side's raw journal locations and identifies either the matched finite
|
||||
variant or the complete satisfied constraint set. A failure returns the first
|
||||
declaration-ordered predicate, cardinality, edge, prefix, suffix, or language
|
||||
violation with its relevant event locations.
|
||||
If both captures have the same invalid declared trace, the nested verdict is
|
||||
`nonconforming` while the top-level capture comparison remains unchanged. Side
|
||||
statuses and the diagnostic distinguish conformance from pairwise difference.
|
||||
|
||||
The result classifies terminal, interaction, exit, filesystem, protocol,
|
||||
process, and shim behavior. `first_divergence` points to the earliest observed
|
||||
difference. Residual unknowns prevent a claim of complete equivalence, but they
|
||||
do not hide an observed difference in another dimension.
|
||||
|
||||
Captures must have equal comparison-contract commitments, but may identify
|
||||
different scenarios and executables. MCP callers may set `max_capture_age_ms`;
|
||||
the application clock then rejects stale captures before comparison.
|
||||
|
||||
## Limits and lifecycle behavior
|
||||
|
||||
Scenarios bound output bytes, frames, files, file bytes, process observations,
|
||||
protocol/shim events, connections, filesystem depth, total runtime, idle time,
|
||||
and settlement time. Process trees are sampled rather than syscall-traced, and
|
||||
short-lived descendants may be missed. Filesystem observations are snapshots,
|
||||
not filesystem event streams.
|
||||
|
||||
REA tags launched processes with a private run identifier. During cleanup it
|
||||
revalidates that identifier before signaling sampled process groups, including
|
||||
groups detached from the original parent. Temporary HOME, replay, shim,
|
||||
terminal, checkpoint, and sampling resources are released on success, failure,
|
||||
timeout, and cancellation.
|
||||
|
||||
After root exit, REA checks token-owned process groups every 50 ms. Two
|
||||
consecutive empty observations establish `quiesced`; the settlement deadline
|
||||
otherwise produces `alive_at_deadline`, while inspection or identity failures
|
||||
produce `unverifiable`. Sampling can still miss short-lived or early-detached
|
||||
descendants, which remains a residual unknown.
|
||||
Each capture carries commitments for the selected scenario, executable,
|
||||
comparison contract, and normalization rules. Comparison rejects captures
|
||||
whose comparison contracts differ. Captures are local Evidence files; keep
|
||||
their source artifacts and invocation context available when interpreting a
|
||||
difference.
|
||||
|
||||
+90
-43
@@ -1,30 +1,34 @@
|
||||
{
|
||||
"package": {
|
||||
"name": "rea-agents",
|
||||
"version": "3.2.1",
|
||||
"version": "4.0.1",
|
||||
"sdk": {
|
||||
"server": "2.0.0",
|
||||
"client": "2.0.0",
|
||||
"core": "2.0.0"
|
||||
"server": "2.3.1",
|
||||
"client": "2.3.1",
|
||||
"core": "2.3.1"
|
||||
},
|
||||
"skill_version": "23"
|
||||
"skill_version": "24"
|
||||
},
|
||||
"tools": {
|
||||
"total": 114,
|
||||
"total": 118,
|
||||
"families": [
|
||||
{
|
||||
"id": "direct",
|
||||
"surface": "official-proxy",
|
||||
"count": 36,
|
||||
"count": 41,
|
||||
"tools": [
|
||||
"address_name",
|
||||
"address_to_file_offset",
|
||||
"annotate_native_function",
|
||||
"comment",
|
||||
"current_address",
|
||||
"current_document",
|
||||
"current_procedure",
|
||||
"goto_address",
|
||||
"inline_comment",
|
||||
"inspect_native_data_type",
|
||||
"inspect_native_instruction",
|
||||
"inspect_native_load_image",
|
||||
"list_bookmarks",
|
||||
"list_documents",
|
||||
"list_names",
|
||||
@@ -43,6 +47,7 @@
|
||||
"read_bytes",
|
||||
"read_function_instructions",
|
||||
"resolve_containing_procedure",
|
||||
"resolve_native_call_targets",
|
||||
"search_procedures",
|
||||
"search_strings",
|
||||
"set_address_name",
|
||||
@@ -64,7 +69,6 @@
|
||||
"analyze_swift_types",
|
||||
"batch_decompile",
|
||||
"binary_overview",
|
||||
"find_code_for_string",
|
||||
"find_xrefs_to_name",
|
||||
"get_call_graph",
|
||||
"get_objc_classes",
|
||||
@@ -73,42 +77,46 @@
|
||||
"inspect_native_dispatch_metadata",
|
||||
"trace_call_path",
|
||||
"trace_feature",
|
||||
"trace_native_ui_action"
|
||||
"trace_native_ui_action",
|
||||
"trace_native_values"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "native",
|
||||
"surface": "native-provider",
|
||||
"count": 5,
|
||||
"count": 7,
|
||||
"tools": [
|
||||
"capture_native_ui_scenario",
|
||||
"demangle_swift",
|
||||
"inspect_macho",
|
||||
"inspect_plist",
|
||||
"inspect_signature",
|
||||
"list_architectures"
|
||||
"list_architectures",
|
||||
"observe_native_ui"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "artifact",
|
||||
"surface": "artifact-provider",
|
||||
"count": 3,
|
||||
"count": 5,
|
||||
"tools": [
|
||||
"decode_interface_builder",
|
||||
"extract_artifact",
|
||||
"inspect_artifact"
|
||||
"inspect_artifact",
|
||||
"inspect_asset_catalog",
|
||||
"inspect_keyed_archive"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "managed",
|
||||
"surface": "managed",
|
||||
"count": 8,
|
||||
"count": 7,
|
||||
"tools": [
|
||||
"compare_managed_members",
|
||||
"import_managed_reconstruction",
|
||||
"inspect_managed_artifact",
|
||||
"inspect_managed_members",
|
||||
"inspect_managed_native_boundaries",
|
||||
"plan_managed_runtime_correlation",
|
||||
"project_managed_application_graph",
|
||||
"verify_managed_native_boundaries"
|
||||
]
|
||||
@@ -153,16 +161,13 @@
|
||||
{
|
||||
"id": "application",
|
||||
"surface": "application-workflow",
|
||||
"count": 10,
|
||||
"count": 7,
|
||||
"tools": [
|
||||
"build_reconstruction_obligation_ledger",
|
||||
"compare_application_versions",
|
||||
"compare_javascript_export_shapes",
|
||||
"compare_source_to_bundle",
|
||||
"evaluate_reconstruction_coverage",
|
||||
"execute_node_characterization",
|
||||
"prepare_node_characterization",
|
||||
"run_controlled_replay",
|
||||
"trace_application_feature",
|
||||
"trace_javascript_semantics"
|
||||
]
|
||||
@@ -170,7 +175,7 @@
|
||||
{
|
||||
"id": "session",
|
||||
"surface": "session",
|
||||
"count": 22,
|
||||
"count": 21,
|
||||
"tools": [
|
||||
"binary_session",
|
||||
"build_call_path",
|
||||
@@ -190,7 +195,6 @@
|
||||
"list_unknowns",
|
||||
"open_binary",
|
||||
"record_unknown",
|
||||
"run_replay_machine",
|
||||
"update_unknown",
|
||||
"verify_reconstruction",
|
||||
"verify_unknown_resolution"
|
||||
@@ -205,7 +209,12 @@
|
||||
"version": null,
|
||||
"capabilities": [
|
||||
"address_name",
|
||||
"address_to_file_offset",
|
||||
"analyze_function",
|
||||
"annotate_native_function",
|
||||
"inspect_native_data_type",
|
||||
"inspect_native_instruction",
|
||||
"inspect_native_load_image",
|
||||
"list_documents",
|
||||
"list_names",
|
||||
"list_procedures",
|
||||
@@ -218,8 +227,10 @@
|
||||
"procedure_info",
|
||||
"procedure_pseudo_code",
|
||||
"procedure_references",
|
||||
"read_bytes",
|
||||
"read_function_instructions",
|
||||
"resolve_containing_procedure",
|
||||
"resolve_native_call_targets",
|
||||
"search_procedures",
|
||||
"search_strings",
|
||||
"xrefs"
|
||||
@@ -274,11 +285,13 @@
|
||||
"name": "macOS native inspection utilities",
|
||||
"version": null,
|
||||
"capabilities": [
|
||||
"capture_native_ui_scenario",
|
||||
"demangle_swift",
|
||||
"inspect_macho",
|
||||
"inspect_plist",
|
||||
"inspect_signature",
|
||||
"list_architectures"
|
||||
"list_architectures",
|
||||
"observe_native_ui"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -288,7 +301,9 @@
|
||||
"capabilities": [
|
||||
"decode_interface_builder",
|
||||
"extract_artifact",
|
||||
"inspect_artifact"
|
||||
"inspect_artifact",
|
||||
"inspect_asset_catalog",
|
||||
"inspect_keyed_archive"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -329,7 +344,6 @@
|
||||
"capabilities": [
|
||||
"compare_managed_members",
|
||||
"import_managed_reconstruction",
|
||||
"plan_managed_runtime_correlation",
|
||||
"project_managed_application_graph",
|
||||
"verify_managed_native_boundaries"
|
||||
]
|
||||
@@ -350,9 +364,6 @@
|
||||
"compare_javascript_export_shapes",
|
||||
"compare_source_to_bundle",
|
||||
"evaluate_reconstruction_coverage",
|
||||
"execute_node_characterization",
|
||||
"prepare_node_characterization",
|
||||
"run_controlled_replay",
|
||||
"trace_application_feature",
|
||||
"trace_javascript_semantics"
|
||||
]
|
||||
@@ -425,20 +436,53 @@
|
||||
{
|
||||
"id": "devin",
|
||||
"display_name": "Devin",
|
||||
"format": "unsupported",
|
||||
"configuration": "detect-only"
|
||||
"format": "json",
|
||||
"configuration": "managed"
|
||||
},
|
||||
{
|
||||
"id": "opencode",
|
||||
"display_name": "OpenCode",
|
||||
"format": "opencode",
|
||||
"configuration": "managed"
|
||||
},
|
||||
{
|
||||
"id": "antigravity",
|
||||
"display_name": "Antigravity",
|
||||
"format": "json",
|
||||
"configuration": "managed"
|
||||
},
|
||||
{
|
||||
"id": "copilot_cli",
|
||||
"display_name": "GitHub Copilot CLI",
|
||||
"format": "copilot_cli",
|
||||
"configuration": "managed"
|
||||
},
|
||||
{
|
||||
"id": "commandcode",
|
||||
"display_name": "Command Code",
|
||||
"format": "commandcode",
|
||||
"configuration": "managed"
|
||||
},
|
||||
{
|
||||
"id": "vscode",
|
||||
"display_name": "VS Code",
|
||||
"format": "vscode",
|
||||
"configuration": "managed"
|
||||
}
|
||||
],
|
||||
"cli": {
|
||||
"primary_count": 67,
|
||||
"primary_count": 73,
|
||||
"commands": [
|
||||
"address-to-file-offset",
|
||||
"analyze",
|
||||
"analyze-javascript-application",
|
||||
"analyze-web-bundle",
|
||||
"annotate-native-function",
|
||||
"build-reconstruction-obligation-ledger",
|
||||
"capabilities",
|
||||
"capture-browser-scenario",
|
||||
"capture-electron-scenario",
|
||||
"capture-native-ui-scenario",
|
||||
"capture-process",
|
||||
"capture-web-screenshot",
|
||||
"compare",
|
||||
@@ -457,20 +501,24 @@
|
||||
"evaluate-reconstruction-coverage",
|
||||
"evidence-export",
|
||||
"evidence-import",
|
||||
"execute-node-characterization",
|
||||
"extract-artifact",
|
||||
"function",
|
||||
"import-managed-reconstruction",
|
||||
"import-reference-source",
|
||||
"inspect",
|
||||
"inspect-artifact",
|
||||
"inspect-asset-catalog",
|
||||
"inspect-electron-page",
|
||||
"inspect-keyed-archive",
|
||||
"inspect-macho",
|
||||
"inspect-managed-artifact",
|
||||
"inspect-managed-members",
|
||||
"inspect-managed-native-boundaries",
|
||||
"inspect-native-api",
|
||||
"inspect-native-data-type",
|
||||
"inspect-native-dispatch-metadata",
|
||||
"inspect-native-instruction",
|
||||
"inspect-native-load-image",
|
||||
"inspect-plist",
|
||||
"inspect-signature",
|
||||
"inspect-web-page",
|
||||
@@ -480,23 +528,22 @@
|
||||
"list-electron-targets",
|
||||
"list-javascript-runtime-targets",
|
||||
"observe-javascript-runtime",
|
||||
"observe-native-ui",
|
||||
"observe-web-session",
|
||||
"plan-managed-runtime-correlation",
|
||||
"policy",
|
||||
"prepare-node-characterization",
|
||||
"project-managed-application-graph",
|
||||
"providers",
|
||||
"read-bytes",
|
||||
"reconcile-javascript-runtime",
|
||||
"run-controlled-replay",
|
||||
"run-replay-machine",
|
||||
"resolve-native-call-targets",
|
||||
"search",
|
||||
"setup",
|
||||
"trace",
|
||||
"trace-application-feature",
|
||||
"trace-javascript-semantics",
|
||||
"trace-native-ui-action",
|
||||
"trace-native-values",
|
||||
"uninstall",
|
||||
"upgrade",
|
||||
"update",
|
||||
"verify-managed-native-boundaries",
|
||||
"xrefs"
|
||||
],
|
||||
@@ -509,15 +556,15 @@
|
||||
},
|
||||
"runtime_catalog": {
|
||||
"counts": {
|
||||
"cli_commands": 67,
|
||||
"mcp_tools": 114,
|
||||
"cli_commands": 73,
|
||||
"mcp_tools": 118,
|
||||
"mcp_prompts": 6
|
||||
},
|
||||
"digests": {
|
||||
"tools_sha256": "eb79567cc08f4afec223254c3977dc167d498f766b920077db0e1eead8be67b9",
|
||||
"prompts_sha256": "fa889fc671185ac9c968c6dc8309cf609e709cdc837dbbf39b7cdfadb84ac2d7",
|
||||
"combined_sha256": "3e9ab4e05563ea2609b7cf4db67638a79cd8a74188c292718d99a63451d5b9db",
|
||||
"providers_sha256": "03498d8adb7d941f8293eea8d71375871fb30a278e310fdc19b98df6781fc1a7"
|
||||
"tools_sha256": "bbd9aacf7bb0bc54a8f8743491583482e1176252513d99780a309d0fddeb3df0",
|
||||
"prompts_sha256": "4762db348821fec787a5b8c6423f2c48a71f6f0ec2afee97b16ad69030ec504b",
|
||||
"combined_sha256": "b4f7e5860d90b029876b58fe7cd4a8041fef1f2add23440e37e90827d808e503",
|
||||
"providers_sha256": "e094c361b60ec930a6062a1a90ee05536ce411541dba5b51ace7e2da2ba25af0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+67
-65
@@ -1,26 +1,25 @@
|
||||
# Static-analysis provider evaluation
|
||||
|
||||
Status: the Ghidra read-only analysis provider is shipped on Linux x64 and has
|
||||
an experimental Windows x64 P0 for approved native PE applications. macOS is
|
||||
not an admitted Ghidra host. The provider
|
||||
validates an exact bring-your-own Ghidra 12.1.4/JDK 21 environment, resolves a
|
||||
provider/version/profile commitment, runs one isolated read-only headless
|
||||
import, and publishes 19 operation-level capabilities after the authenticated
|
||||
post-analysis handshake.
|
||||
Ghidra read-only analysis is available on Linux x64 and macOS x64/arm64.
|
||||
Install Ghidra 12.1.4 and a full 64-bit JDK 21 separately, then configure REA to
|
||||
use them. macOS installations need the matching native decompiler.
|
||||
|
||||
The provider-neutral target, provider registry, deterministic target binding,
|
||||
analysis-profile commitment, Evidence provenance, snapshot caching, and bounded
|
||||
provider-process lifecycle foundations are implemented. The Ghidra launcher,
|
||||
packaged Java bridge, doctor/setup projection, bounded client lifecycle, and
|
||||
multi-target real Linux verifier, curated Windows CI, and controlled real
|
||||
Windows verifier are also implemented. Program identity,
|
||||
procedure/string/symbol inventory, memory blocks, address/name and
|
||||
containing-procedure resolution, complete inventory search, function metadata,
|
||||
decompilation, assembly, resolved calls, typed references, xrefs, CFG, and
|
||||
function dossiers are admitted. A provider is not a drop-in replacement for
|
||||
Hopper: every capability is mapped explicitly, with truthful `unavailable`,
|
||||
unknown, or degraded results where the engine cannot provide equivalent
|
||||
semantics.
|
||||
REA imports one target into a temporary project and exposes 25 read-only
|
||||
operations after analysis completes. They cover inventories, search,
|
||||
decompilation, assembly, function metadata, resolved calls, references, control
|
||||
flow, instructions, and recovered types. Results identify the provider version,
|
||||
analysis settings, target digest, and any unavailable or incomplete facts.
|
||||
On Linux and macOS, `annotate_native_function` additionally edits names and entry
|
||||
comments atomically, verifies readback, and returns refreshed function analysis.
|
||||
Changes live only in the session database and leave executable bytes unchanged.
|
||||
Ghidra does not expose GUI controls; Windows P0 remains read-only.
|
||||
|
||||
Windows x64 P0 supports native x86-64 PE applications on local NTFS with
|
||||
bundled Job Object ownership, protected runtime DACLs, and handle-based path
|
||||
admission. Real ordinary-user verification covers all 25 read-only operations
|
||||
through the packaged CLI and MCP, target integrity, and cleanup. This remains
|
||||
an experimental boundary; see [Windows Ghidra P0](windows-ghidra-p0.md) and
|
||||
[issue #527](https://github.com/morluto/rea/issues/527).
|
||||
|
||||
[ADR-0001](adr/0001-provider-selection-and-analysis-profiles.md) fixes the
|
||||
provider registry, deterministic selection, target binding, analysis profile,
|
||||
@@ -32,8 +31,8 @@ follow.
|
||||
- Keep the existing provider-neutral CLI and MCP tool names.
|
||||
- Bind one deep-analysis provider to a target for the target's lifetime; never
|
||||
fail over silently between Hopper and Ghidra.
|
||||
- Start with bring-your-own Ghidra and a compatible Java runtime. Setup must not
|
||||
install or upgrade Java.
|
||||
- Use an existing Ghidra installation and a compatible Java runtime. Setup must
|
||||
not install or upgrade Ghidra or Java.
|
||||
- Run Ghidra headlessly in an owned process with a private temporary project,
|
||||
startup and cleanup deadlines, caller cancellation, and complete replies.
|
||||
- Prefer a packaged Java bridge loaded through Ghidra's script path. PyGhidra
|
||||
@@ -54,10 +53,10 @@ follow.
|
||||
|
||||
`GHIDRA_INSTALL_DIR` must identify an extracted official 12.1.4 release;
|
||||
optional `JAVA_HOME` must identify a 64-bit full JDK 21, otherwise doctor probes
|
||||
`java`/`javac` or `java.exe`/`javac.exe` from `PATH`. Linux accepts x86, x86-64,
|
||||
ARM, and ARM64 ELF, PE, and Mach-O executable targets. Windows P0 accepts only
|
||||
native x86-64 PE applications and rejects DLLs, managed images, unknown roles,
|
||||
other formats, and other architectures before launch.
|
||||
`java`/`javac` or `java.exe`/`javac.exe` from `PATH`. Supported Linux and macOS
|
||||
hosts accept compatible ELF, PE, and Mach-O executable targets. Host admission
|
||||
and target compatibility are separate checks. The intended Windows P0 accepts
|
||||
only native x86-64 PE applications; it remains blocked before launch.
|
||||
|
||||
The launcher creates one ephemeral runtime root with project,
|
||||
home/cache/config/data/temp, logs, descriptor, endpoint, target snapshot, and
|
||||
@@ -66,18 +65,19 @@ It passes `-readOnly` and `-deleteProject`, and uses Ghidra's default analysis
|
||||
and resource settings; inherited Java option injection variables are cleared.
|
||||
On Linux, the mode-0600 descriptor carries the random token without
|
||||
exposing it in argv or environment and the Java bridge binds a mode-restricted
|
||||
Unix socket. Windows uses authenticated IPv4 loopback with a strict token-free
|
||||
endpoint record. Both transports report actual
|
||||
Ghidra/language/compiler/analysis/import-digest metadata, accept only the exact
|
||||
authenticated `ping`, `shutdown`, and ten
|
||||
inventory plus nine function-analysis methods, and deletes the socket. Close,
|
||||
cancellation, timeout,
|
||||
malformed protocol, or process exit stops the owned process resources and
|
||||
removes the entire runtime root. Windows P0 uses bounded `taskkill` tree
|
||||
termination and explicitly does not claim Job Object, private-DACL, or
|
||||
reparse-point authority; see [Windows Ghidra P0](windows-ghidra-p0.md).
|
||||
Unix socket. macOS uses the same local transport. The bridge reports actual
|
||||
Ghidra/language/compiler/analysis/import-digest metadata and accepts only
|
||||
authenticated `ping`, `shutdown`, ten inventory methods, and twelve
|
||||
function-analysis methods. Close, cancellation, timeout, malformed protocol,
|
||||
or process exit stops the owned process resources, closes the socket, and
|
||||
removes the runtime root.
|
||||
|
||||
The provider catalog lists only the 19 proved Ghidra operations. GUI cursor,
|
||||
The experimental Windows transport uses authenticated IPv4 loopback with a
|
||||
token-free endpoint record, but cannot start while the native authority is
|
||||
unavailable. Its `taskkill` cleanup code does not establish Job Object ownership,
|
||||
private DACLs, or reparse-safe paths.
|
||||
|
||||
The provider catalog lists the 25 Ghidra operations. GUI cursor,
|
||||
navigation, and mutation operations remain absent; the router therefore
|
||||
reports them unavailable instead of borrowing Hopper semantics or inferring
|
||||
capability from a successful import.
|
||||
@@ -98,21 +98,21 @@ capability from a successful import.
|
||||
|
||||
## Admitted function-analysis semantics
|
||||
|
||||
| Concern | Ghidra contract |
|
||||
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Decompiler lifetime | One persistent `DecompInterface` is opened for the imported Program and disposed during bridge shutdown. Native decompilation has no fixed per-function deadline. External functions or functions without bodies return `null`; cancellation and native failure remain distinct. |
|
||||
| Serialization | A FIFO sends one Program request at a time. Caller cancellation removes queued work promptly and is passed to active socket waits. This is an adapter safety commitment, not a claim that every Ghidra API is thread-safe. |
|
||||
| Function identity | Every function result carries the entry address and Ghidra FunctionManager classification for external, thunk, and resolved thunk target. These are observations, not proof that unresolved targetless calls have been recovered. |
|
||||
| Assembly and pseudocode | Assembly is complete Ghidra Listing text; pseudocode is Ghidra decompiler output. Neither is original source, and cross-provider comparison never treats Hopper and Ghidra text as equal or unequal semantic facts. |
|
||||
| Instruction fast path | `read_function_instructions` returns every raw Listing instruction for the requested function without invoking the decompiler or whole-program name/string inventories. |
|
||||
| Calls and references | Callers/callees contain only resolved functions. Reference edges preserve exact ReferenceManager type and call/jump/data/read/write/indirect/computed/conditional/terminal/external facts. Targetless computed flow remains unknown. Synthetic entry-point references without actionable memory sources are omitted explicitly. |
|
||||
| CFG | Dossiers use `BasicBlockModel` and retain only non-call successors inside the function body. CFG topology is address-normalized for comparison; provider-specific block construction remains a declared difference. |
|
||||
| P-code value flow | Function dossiers include up to 3,000 high-p-code operations, at most 64 inputs per operation, and 12,000 def-use edges. Results identify memory reads/writes, branches, and calls by p-code opcode, and mark decompiler-dead operations. Large results report truncation, an omitted-operation lower bound, and known omitted-input/edge counts. This is an intra-function decompiler graph: it does not resolve cross-function data flow, memory aliasing, call side effects, runtime behavior, persistent state, or business meaning. |
|
||||
| Result extent | Function instruction scans and native API boundary observations remain complete. P-code flow is the explicit bounded exception; its count fields distinguish exact retained counts from lower-bound or known-omitted counts. Caller cancellation and provider failures remain distinct. |
|
||||
| Concern | Ghidra contract |
|
||||
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Decompiler lifetime | One persistent `DecompInterface` is opened for the imported Program and disposed during bridge shutdown. Native decompilation has no fixed per-function deadline. External functions or functions without bodies return `null`; cancellation and native failure remain distinct. |
|
||||
| Serialization | A FIFO sends one Program request at a time. Caller cancellation removes queued work promptly and is passed to active socket waits. This is an adapter safety commitment, not a claim that every Ghidra API is thread-safe. |
|
||||
| Function identity | Every function result carries the entry address and Ghidra FunctionManager classification for external, thunk, and resolved thunk target. These are observations, not proof that unresolved targetless calls have been recovered. |
|
||||
| Assembly and pseudocode | Assembly is complete Ghidra Listing text; pseudocode is Ghidra decompiler output. Neither is original source, and cross-provider comparison never treats Hopper and Ghidra text as equal or unequal semantic facts. |
|
||||
| Instruction fast path | `read_function_instructions` returns every raw Listing instruction for the requested function without invoking the decompiler or whole-program name/string inventories. |
|
||||
| Calls and references | Callers/callees contain only resolved functions. Reference edges preserve exact ReferenceManager type and call/jump/data/read/write/indirect/computed/conditional/terminal/external facts. Targetless computed flow remains unknown. Synthetic entry-point references without actionable memory sources are omitted explicitly. |
|
||||
| CFG | Dossiers use `BasicBlockModel` and retain only non-call successors inside the function body. CFG topology is address-normalized for comparison; provider-specific block construction remains a declared difference. |
|
||||
| P-code value flow | Function dossiers include up to 3,000 high-p-code operations, at most 64 inputs per operation, and 12,000 def-use edges. Results identify memory reads/writes, branches, and calls by p-code opcode, and mark decompiler-dead operations. Large results report truncation, an omitted-operation lower bound, and known omitted-input/edge counts. The dossier is an intra-function graph; `trace_native_values` composes derived parameter/argument and return/output dependencies across resolved calls. It does not resolve memory aliasing, call side effects, runtime behavior, persistent state, or business meaning. |
|
||||
| Result extent | Function instruction scans and native API boundary observations remain complete. P-code flow is the explicit bounded exception; its count fields distinguish exact retained counts from lower-bound or known-omitted counts. Caller cancellation and provider failures remain distinct. |
|
||||
|
||||
`npm run verify:ghidra` compiles the versioned C oracle into debug and stripped
|
||||
host-native targets (x86-64 ELF on Linux x64). It proves all
|
||||
19 operations, external functions, resolved thunks, exports, stripped-name
|
||||
host-native targets (x86-64 ELF on Linux x64 or Mach-O on macOS), plus a native
|
||||
DWARF 4 type-layout object. It proves the admitted operations, external functions, resolved thunks, exports, stripped-name
|
||||
behavior, direct and targetless indirect calls, typed references, strings/xrefs,
|
||||
multi-block CFG, bounded p-code def-use/effect links, semantic enhanced workflows, cancellation, startup deadlines,
|
||||
serialized concurrency, malformed-target rejection, profile identity, and
|
||||
@@ -126,24 +126,26 @@ Mach-O target coverage. It requires `clang`, LLD, and `lld-link`; set
|
||||
these tools before compilation. Keeping this matrix separate lets Linux
|
||||
host/provider acceptance run with only the host compiler.
|
||||
|
||||
`npm run verify:ghidra:windows` generates a deterministic source-owned native
|
||||
x86-64 PE application, proves all 19 operations through the production Windows
|
||||
launcher and loopback transport, checks target/snapshot/import SHA-256 linkage,
|
||||
and requires project, endpoint, process, and runtime cleanup. Passing that lane
|
||||
proves only the documented P0 boundary, not the remaining Windows security
|
||||
gates.
|
||||
`npm run verify:ghidra:windows` checks the source-owned native x86-64 PE
|
||||
fixture, all 25 read-only operations, target/snapshot/import SHA-256 linkage,
|
||||
and project, endpoint, process, and runtime cleanup. The independent native
|
||||
lane checks DACLs, handle admission, cancellation, and Job Object lifecycle.
|
||||
`npm run verify:ghidra:windows:package` packs and installs REA into an isolated
|
||||
prefix, then checks ordinary-user CLI/MCP operations against canonical contracts.
|
||||
All lanes require matching native controls; package startup alone establishes
|
||||
only package compatibility.
|
||||
|
||||
## Shared provider-process foundation
|
||||
|
||||
`src/process/` now provides the mechanisms that a long-lived Hopper or Ghidra
|
||||
adapter genuinely shares: POSIX run-token-authenticated process-group ownership,
|
||||
adapter shares: POSIX run-token-authenticated process-group ownership,
|
||||
ephemeral temporary runtime roots, one absolute startup deadline, correlated
|
||||
request cancellation and lifecycle-deadline cleanup, bounded stdout and stderr retention with
|
||||
exact byte counts, process-exit diagnostics, and bounded TERM-to-KILL shutdown.
|
||||
Reusable fixtures exercise exit, timeout, cancellation, graceful termination,
|
||||
forced termination, double-close, spawn failure, and resource release. Windows
|
||||
P0 adds bounded process-tree termination as an explicitly weaker host backend;
|
||||
Job Object ownership remains unimplemented.
|
||||
process-tree termination code exists, but the missing Job Object and private
|
||||
runtime controls prevent it from establishing an available Ghidra session.
|
||||
|
||||
The foundation does not define a bridge schema, socket framing, health payload,
|
||||
analysis model, or shutdown acknowledgement. Hopper keeps its authenticated
|
||||
@@ -153,12 +155,12 @@ only the generic process mechanisms.
|
||||
|
||||
## Shortlist
|
||||
|
||||
| Provider | License / automation surface | What it brings | REA fit and blockers |
|
||||
| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [Ghidra](https://github.com/NationalSecurityAgency/ghidra) | Apache-2.0 source license; `analyzeHeadless`, Java APIs, and PyGhidra | Broad static analysis, many processors and formats, scripting, and project/database workflows | Read-only analysis shipped: exact BYO checks, isolated state and provider lifecycle, serial API queue, packaged bridge, 19 inventory/function operations, and real ELF/PE/Mach-O x86-64 plus AArch64 conformance. GUI and mutation semantics remain intentionally unavailable. |
|
||||
| [Rizin](https://github.com/rizinorg/rizin) / [rz-pipe](https://github.com/rizinorg/rz-pipe) | Rizin repository contains LGPL-3.0 and GPL-3.0 components; `rizin`, `rz-bin`, and language bridges through `rzpipe` | Portable CLI analysis, disassembly/debugging, many architectures and file formats, JSON command output | Good candidate for a process-backed Linux provider and fast metadata fallback. License/component inventory must be preserved; command output needs version-pinned parsers and semantic conformance before evidence is trusted. |
|
||||
| [LIEF](https://github.com/lief-project/LIEF) | Apache-2.0; C++, Python, and other bindings | Deterministic parsing and modification of ELF, PE, Mach-O, COFF, and related executable formats; headers, sections, symbols, relocations, and functions | Best near-term complement, not a decompiler replacement. It can cover format metadata and artifact evidence without a long-lived analysis process; function semantics, pseudocode, CFG, and cross-reference parity remain out of scope unless separately demonstrated. |
|
||||
| [Binary Ninja](https://docs.binary.ninja/dev/index.html) | API/documentation components are MIT, while the analysis product is licensed by edition; commercial, Ultimate, or Headless license is required for headless automation | Python/Core/C++/Rust APIs, headless loading, IL layers, function analysis, plugins, and configurable analysis | Strong technical fit for a native provider, especially function dossiers. Commercial licensing, license-secret handling, native runtime packaging, and multithreaded lifecycle rules are material deployment blockers. |
|
||||
| Provider | License / automation surface | What it brings | REA fit and blockers |
|
||||
| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Ghidra](https://github.com/NationalSecurityAgency/ghidra) | Apache-2.0 source license; `analyzeHeadless`, Java APIs, and PyGhidra | Static analysis, multiple processors and formats, scripting, and project/database workflows | 25 read-only operations shipped on Linux/macOS with installation checks, temporary projects, a serial API queue, and real ELF/PE/Mach-O conformance. Experimental Windows x64 P0 supports native x86-64 PE on local NTFS with bundled native controls and ordinary-user CLI/MCP verification; Linux/macOS additionally support atomic session function annotations; Windows mutation and all Ghidra GUI operations are unavailable. |
|
||||
| [Rizin](https://github.com/rizinorg/rizin) / [rz-pipe](https://github.com/rizinorg/rz-pipe) | Rizin repository contains LGPL-3.0 and GPL-3.0 components; `rizin`, `rz-bin`, and language bridges through `rzpipe` | Portable CLI analysis, disassembly/debugging, many architectures and file formats, JSON command output | Good candidate for a process-backed Linux provider and fast metadata fallback. License/component inventory must be preserved; command output needs version-pinned parsers and semantic conformance before evidence is trusted. |
|
||||
| [LIEF](https://github.com/lief-project/LIEF) | Apache-2.0; C++, Python, and other bindings | Deterministic parsing and modification of ELF, PE, Mach-O, COFF, and related executable formats; headers, sections, symbols, relocations, and functions | Best near-term complement, not a decompiler replacement. It can cover format metadata and artifact evidence without a long-lived analysis process; function semantics, pseudocode, CFG, and cross-reference parity remain out of scope unless separately demonstrated. |
|
||||
| [Binary Ninja](https://docs.binary.ninja/dev/index.html) | API/documentation components are MIT, while the analysis product is licensed by edition; commercial, Ultimate, or Headless license is required for headless automation | Python/Core/C++/Rust APIs, headless loading, IL layers, function analysis, plugins, and configurable analysis | Strong technical fit for a native provider, especially function dossiers. Commercial licensing, license-secret handling, native runtime packaging, and multithreaded lifecycle rules are material deployment blockers. |
|
||||
|
||||
## Recommended order
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ check project-owned conformance fixtures and fail-closed report behavior.
|
||||
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| `discover-classify` | target-free startup, artifact inventory, compatible workflow selection, and exact visible limits |
|
||||
| `diagnose-environment` | broken runtime separated from analysis failure, version skew detected, and revision changed after recovery |
|
||||
| `acquire-authority` | exact missing scope, narrow grant, expansion re-prompt, and no launch after denial/cancellation |
|
||||
| `declare-request` | exact target, operation, and lifecycle are carried by the request before launch |
|
||||
| `static-analysis` | native and JavaScript routing plus preserved ambiguity, truncation, unsupported syntax, and residual unknowns |
|
||||
| `reactive-scenarios` | predicate-driven interaction, descendant settlement, Electron correlation, and cancellation cleanup Evidence |
|
||||
| `compare-authority-candidate` | explicit partial-order/finite-trace semantics, primary divergence localization, and unknown for incomplete comparison |
|
||||
@@ -63,7 +63,7 @@ The evaluator rejects or withholds readiness when:
|
||||
- contradiction Evidence or its affected comparison is missing;
|
||||
- the reconstruction obligation ledger is not `ready`, has no required
|
||||
obligation, or retains an unowned/unverified required obligation;
|
||||
- denied authority launches a process, cancellation loses diagnostics, or owned
|
||||
- a process exceeds its declared target/lifecycle, cancellation loses diagnostics, or owned
|
||||
resources remain;
|
||||
- replay is nondeterministic, tampering/staleness is not detected, or the
|
||||
expected source digest differs.
|
||||
|
||||
+69
-32
@@ -1,32 +1,69 @@
|
||||
# Installation roadmap
|
||||
# REA roadmap
|
||||
|
||||
## Tool direction
|
||||
|
||||
REA provides inspection, capture, and comparison tools that return evidence
|
||||
directly. Agents choose commands, paths, browser actions, scripts, and local
|
||||
fixture servers, then compose experiments from those results. CLI and MCP must
|
||||
expose equivalent behavior and preserve observations, inferences, and unknowns.
|
||||
|
||||
[#555](https://github.com/morluto/rea/pull/555) removed REA permission grants,
|
||||
scope ceilings, elicitation, and repeated approval fields.
|
||||
[#572](https://github.com/morluto/rea/pull/572) removed the replay engines,
|
||||
Node characterization prepare/execute flow, and plan-only managed runtime
|
||||
correlation tool. Those systems are retired roadmap items. Existing evidence
|
||||
authority labels remain readable as provenance; they do not imply a current
|
||||
replay executor.
|
||||
|
||||
Local operations use the current user's OS permissions. Input and protocol
|
||||
validation, provider prerequisites, target identity, cancellation, and owned
|
||||
resource cleanup remain part of each tool's contract. Setup still discloses
|
||||
installation changes and requires approval before writing or installing.
|
||||
|
||||
## Remaining evidence and provider work
|
||||
|
||||
The [platform roadmap](https://github.com/morluto/rea/issues/32),
|
||||
[process and Hopper fidelity tracker](https://github.com/morluto/rea/issues/48),
|
||||
and [browser tracker](https://github.com/morluto/rea/issues/39) track remaining
|
||||
capabilities and proof. Their scope follows direct tools and agent-composed
|
||||
experiments; custom replay languages and grant systems are not completion gates.
|
||||
|
||||
[Inline Evidence composition](https://github.com/morluto/rea/pull/585) now returns
|
||||
the complete record accepted by comparison tools. Managed inspection uses an
|
||||
explicit PE/CLI target independently of the native provider selection. Comparisons
|
||||
preserve unknowns when observation is incomplete, and PTY diagnostics retain the
|
||||
actual loader or startup failure. Browser cleanup reports incomplete teardown
|
||||
alongside the primary failure; transient documents invalidate only the affected
|
||||
frame's WebMCP registrations. Real-provider and platform claims still require
|
||||
their corresponding verification lanes.
|
||||
|
||||
## Shipped behavior
|
||||
|
||||
REA setup currently configures the capabilities that exist today: its bundled
|
||||
skill, detected agent integrations, optional Hopper provider, and validated
|
||||
bring-your-own Ghidra paths. It detects
|
||||
Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, and Devin. The
|
||||
first six have documented local MCP configuration boundaries and can be updated
|
||||
additively; Devin is reported but left unchanged.
|
||||
REA setup lets you select agent integration and optional Hopper installation.
|
||||
It installs the bundled workflow, configures detected agents, and can save
|
||||
verified paths for an existing Ghidra installation. It configures Claude Code,
|
||||
Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin, OpenCode, Antigravity,
|
||||
GitHub Copilot CLI, Command Code, and VS Code using each client's configuration format.
|
||||
|
||||
The Ghidra foundation supports Linux x64 with exact Ghidra 12.1.4 and a 64-bit
|
||||
full JDK 21. macOS is not an admitted Ghidra host. An experimental
|
||||
Windows x64 P0 admits approved native x86-64 PE
|
||||
applications through the same 19-operation Java bridge. Doctor validates those coordinates; approved Linux setup propagates them
|
||||
to MCP registrations without installing or modifying either dependency. REA's
|
||||
packaged Java bridge then proves an isolated read-only headless import,
|
||||
post-analysis handshake, complete cleanup, ten admitted read-only inventory
|
||||
operations, and nine function-analysis operations. Real source-owned x86-64
|
||||
debug/stripped ELF, AArch64 ELF, PE, and Mach-O fixtures cover program,
|
||||
procedure, string, symbol, external/thunk, memory, resolution, search,
|
||||
decompilation, assembly, calls, typed references, xrefs, and CFG semantics.
|
||||
Function dossiers also expose bounded Ghidra high-p-code def-use and memory or
|
||||
control-flow effects, mark decompiler-dead operations, and report truncation
|
||||
plus known omitted counts. This is intra-function
|
||||
decompiler evidence; cross-function state flow remains unimplemented.
|
||||
Hosted Windows CI covers build, package, target admission, transport, and
|
||||
lifecycle seams; a controlled self-hosted workflow covers the real Ghidra P0
|
||||
claim.
|
||||
Ghidra analysis supports Linux x64 and macOS x64/arm64 with Ghidra 12.1.4 and a
|
||||
64-bit full JDK 21. macOS also requires the matching native decompiler. The
|
||||
adapter exposes thirteen inventory operations and twelve function-analysis
|
||||
operations, for 25 read-only operations total. Linux and macOS additionally support
|
||||
atomic function-name and entry-comment edits with refreshed analysis; metadata
|
||||
is discarded on close and executable bytes stay unchanged. Approved setup saves verified
|
||||
installation paths in agent configurations without installing or changing
|
||||
Ghidra or Java.
|
||||
|
||||
Real-provider verification uses host-native debug and stripped fixtures plus
|
||||
a native type-layout object. A separate lane covers AArch64 ELF, PE, and
|
||||
Mach-O cross-target fixtures. See [testing](testing.md) for the prerequisites
|
||||
and exact commands.
|
||||
|
||||
Experimental Windows x64 P0 now supplies bundled Job Object ownership,
|
||||
protected runtime DACLs, and handle-based admission for native x86-64 PE
|
||||
applications on local NTFS. Real ordinary-user CLI/MCP verification covers
|
||||
all 25 read-only operations and cleanup. The [Windows P0 guide](windows-ghidra-p0.md)
|
||||
describes this boundary and unverified broader coverage.
|
||||
|
||||
## Ghidra maintenance boundary
|
||||
|
||||
@@ -36,16 +73,16 @@ not expected to match; unresolved targetless flow remains unknown. Automatic
|
||||
Ghidra acquisition, if ever added, remains a separately planned and approved
|
||||
related-tool change; setup must never install Java.
|
||||
|
||||
Before Windows advances beyond P0, add current-user-only DACL creation and
|
||||
readback, handle-based reparse-point-safe path authority, a DACL-protected IPC
|
||||
backend, and Job Object assignment before provider execution. Process capture,
|
||||
controlled replay, Hopper, and broad filesystem-sensitive workflows remain
|
||||
separate Windows projects rather than implied parity.
|
||||
Maintain Windows P0 with independent DACL readback, handle-based admission,
|
||||
private authenticated IPC, and Job Object assignment before provider execution.
|
||||
Process capture, Hopper, and broad filesystem-sensitive workflows remain
|
||||
separate Windows projects; P0 does not imply their parity.
|
||||
|
||||
## Capability-selective setup
|
||||
|
||||
After REA ships at least two real optional toolchains, setup can evolve into a
|
||||
capability-oriented wizard. It may ask whether the operator wants to investigate
|
||||
Setup already lets users select agent integration and Hopper installation.
|
||||
Once REA supports installing additional analysis tools, it can expand these
|
||||
choices to match the work a user wants to do. It may ask whether the operator wants to investigate
|
||||
native binaries, websites, mobile applications, firmware, or runtime protocols,
|
||||
then propose only the tools needed for the selected work.
|
||||
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
# Test suite audit
|
||||
|
||||
## Scope and priority
|
||||
|
||||
The expanded audit on 2026-10-04 reviewed every one of the 355 Vitest files
|
||||
present at its start: 82 domain/contracts, 56 application/composition, 147
|
||||
adapter/configuration/process/filesystem/CLI files, and 70 MCP/acceptance/
|
||||
conformance/evaluation files. Three review passes checked the proposed removals
|
||||
against existing stronger coverage and checked the final changes for lost
|
||||
assertions. The remaining suite has 351 files.
|
||||
|
||||
Prefer full real-provider E2E, then integration across schemas and transports,
|
||||
then real-data goldens. Remove lower-level duplication once the replacement
|
||||
asserts the same behavior. Keep distinct failure regressions that a successful
|
||||
E2E cannot exercise. Do not lower coverage thresholds to accommodate pruning.
|
||||
|
||||
## Changes made
|
||||
|
||||
| Previous test or assertion | Decision | Replacement or retained claim |
|
||||
| ----------------------------------------------------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Asset application tests using fabricated `.car` bytes and injected utility JSON | Remove two plumbing cases | `verify:asset-catalog`: real actool → assetutil → CLI and stdio MCP, including digest, full metadata, pagination and invalid input |
|
||||
| Keyed archive positive binary/XML objects assembled with the same plist library as the reader | Remove two synthetic success cases | Captured Foundation XML plus reviewed complete object/reference graph golden; real Foundation binary/XML → CLI and stdio MCP |
|
||||
| Generic contract schema/annotation/example loops and output-map inventory comparisons | Consolidate into one SDK wire test | Assert exported schemas, guidance, effect annotations and valid examples on the actual advertised MCP catalog |
|
||||
| Ghidra function operation enum membership | Remove | Canonical catalog, provider handshake and actual operation roundtrips establish admission; malformed provider results remain covered |
|
||||
| Asset page assertion pinning `10_001` | Remove | Invalid-page rejection through real MCP; malformed utility data still covered |
|
||||
| Suggested prompt tool positions | Remove incidental ordering assertions | Keep authority, evidence, untrusted input and preparation constraints |
|
||||
| Package “acceptance” assertions pinning CI matrix/shard values and fake launcher command arrays | Remove configuration-shaped cases | Actual package/install verifier remains; retain a compact release verification-before-publish and registry-canary static guard |
|
||||
| Package failure classifier under acceptance | Move to package evaluation | Preserve all malformed/unrelated-failure rejection cases; do not call them E2E |
|
||||
| Native value graph checks conditional on success | Strengthen | Require success before pagination/ambiguity assertions; unexpected errors can no longer pass |
|
||||
| Native UI verifier treating permission denial as a positive pass | Separate outcomes | Default lane requires real capture/action success; opt-in permissions lane explicitly reports no positive E2E proof |
|
||||
|
||||
Ten previous test cases were removed or consolidated; three replacement
|
||||
Vitest cases cover the captured goldens and release guard. The two real artifact
|
||||
E2E scripts add product-level evidence outside the deterministic Vitest count.
|
||||
|
||||
## Exhaustive follow-up
|
||||
|
||||
The follow-up removes or consolidates another 31 test declarations, including
|
||||
three redundant test files. Parameterized declarations and executed cases have
|
||||
different counts; the figures above count source declarations. It also repairs
|
||||
assertions that could pass without exercising their stated behavior.
|
||||
|
||||
| Area | Cleanup | Stronger retained evidence |
|
||||
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| Configuration | Merge duplicate target/loader tests and preserve custom launcher, explicit provider and actionable error diagnostics | Independent malformed inputs; each capability flag is rejected separately |
|
||||
| Browser scenarios | Remove fake launch success and expression-selected artifact success | Real Chromium scenarios, actual artifacts, CLI capture and owned/external cleanup in `verify:browser` |
|
||||
| CLI policy | Remove duplicate confirmation helper cases | Compiled CLI proves project files stay unchanged until `--yes`; distinct interactive choices remain |
|
||||
| Provider capabilities | Remove numerical catalog freezes; consolidate Hopper byte-read discovery | Exact canonical capability identities, admission, mutation effects and detached descriptors |
|
||||
| Evidence ledger | Merge reordered-key checks into serialized bundle import; remove one-record quota claims | Atomic import, conflicts, residual revisions, actual large collections and byte regressions |
|
||||
| MCP batching and concurrency | Replace maximum concurrency and empty indistinguishable responses | Every distinct ordered batch result; concurrently gated requests complete in reverse order with matching payloads |
|
||||
| MCP inventories and graphs | Remove traversal/inventory call order and redundant catalog/prose checks | Exact complete cyclic graph, full inline inventories, canonical SDK catalog and all-handler roundtrips |
|
||||
| Authority and lifecycle | Attempt the refused Ghidra mutation; require successful setup, call and switch; close twice | Typed refusal with zero mutation dispatch, restored target calls and exact owned-resource cleanup |
|
||||
| Process and provider output | Remove incidental environment read and command option order/defaults | PID/ownership validation, exact disabled-policy failure and complete decoded response above 10 MiB |
|
||||
| Conformance and CI guards | Merge identical replay runs; remove duplicate missing-row fault, trivial hash/self equality and incidental worker/step counts | Distinct faulty evidence, digest order, actual test discovery, file existence and release/runner authority |
|
||||
| Pure schemas | Remove fabricated comparison-dimension injection and impossible long boundary success | Real artifact comparison and SDK coverage evaluation with returned boundary identity |
|
||||
|
||||
The removed long boundary selector test only parsed a 1,000-character request;
|
||||
workspace boundary identities admit at most 200 characters, so that positive
|
||||
could never represent a successful evaluation. The actual MCP evaluation stays
|
||||
covered. One unused ledger serialization helper was also retired after removing
|
||||
its only test consumer; no CLI or MCP contract changed.
|
||||
|
||||
No further immediately safe pruning was identified in the final review. Retained
|
||||
replacement candidates below are coverage gaps, rather than permission to delete
|
||||
unique tests. Test-only domain scaffolding also stays pending a separate decision
|
||||
about the corresponding source behavior; deleting those tests alone would leave
|
||||
unverified code and fail unused-export checks.
|
||||
|
||||
## Stronger replacements from the closing pass
|
||||
|
||||
Three more synthetic success cases were removed. Native value composition now
|
||||
runs through the production CLI and a separate stdio MCP subprocess with real
|
||||
Ghidra, using a source-built native fixture. It checks argument/parameter/return
|
||||
relationships, the fixture's global operand, authenticated Evidence, upstream
|
||||
and workflow analysis profiles, identical CLI/MCP graphs, discovery availability
|
||||
and successful MCP session closure. Ambiguity, cancellation, target changes,
|
||||
missing flow and bounded-work regressions remain in the focused suite.
|
||||
|
||||
That replacement found missing CLI workflow routing for `trace_native_values`
|
||||
and missing workflow profile classification. The routing and discovery now use
|
||||
the composed operation's actual provider prerequisites; workflow Evidence commits
|
||||
to the upstream Ghidra profile.
|
||||
|
||||
The lone hand-built client-document equality test is replaced by a real TOML
|
||||
filesystem transaction: reversed environment keys leave the original bytes
|
||||
unchanged and create no backup. A real ZIP entry with an application root longer
|
||||
than 5,000 characters replaces injected schema-only positive data. Inventory
|
||||
completeness tests partition actual scans into authenticated partial observations
|
||||
and require exact assembled results and every source citation, including more
|
||||
than 100 citations. These are consumer merge tests, not provider pagination
|
||||
claims. Replay aggregation now covers all four statuses and retains actual runner
|
||||
diagnostics; it does not claim to execute a real sandbox. Retained pixel-metric
|
||||
fixtures now write correct PNG chunk checksums.
|
||||
|
||||
The all-zero screenshot buffer is replaced by a real Chromium PNG of 9,467,940
|
||||
bytes, transported intact through CLI and stdio MCP and decoded by the production
|
||||
pixel comparator. The verifier configures the SDK client's receive buffer because
|
||||
its 10 MiB default is smaller than the inline JSON response. This does not claim
|
||||
large comparison-request transport coverage. A real SDK initialize request,
|
||||
captured during a successful production stdio handshake, replaces the JSON-RPC
|
||||
decoder's field-count-only oracle with exact field/value expectations.
|
||||
|
||||
The closing pass also removed assertions tied to internal call order, defaults,
|
||||
private cache sizes, duplicated polling and fabricated schema extensions. Unique
|
||||
malformed-input, security-capacity and cleanup regressions remain.
|
||||
|
||||
## Real process regression found
|
||||
|
||||
The full suite exposed a renderer ordering race. Publishing a terminal output
|
||||
journal entry could synchronously trigger a reactive resize before the preceding
|
||||
output was queued for rendering. Queue that output first. The existing real
|
||||
source-owned interactive PTY test now also requires the first resized frame to
|
||||
contain the output that triggered the resize, preserving the original event
|
||||
order rather than sorting or changing timestamps to conceal it.
|
||||
|
||||
## Coverage kept
|
||||
|
||||
Malformed archives, broken and cyclic references, unsupported metadata pointers,
|
||||
relative Objective-C method layouts, ambiguous call candidates, digest mismatch,
|
||||
cancellation, host permission denial, bounded output and owned-resource cleanup
|
||||
remain covered. These cases provide evidence beyond a successful real-tool run.
|
||||
The hand-assembled relative method fixture stays until compiler-produced
|
||||
relative-list fixtures establish equivalent assertions.
|
||||
|
||||
## Remaining priorities
|
||||
|
||||
1. Extend the real Ghidra CLI/stdio MCP journey from value tracing to dispatch
|
||||
and UI handlers before pruning their hand-authored composition success cases.
|
||||
Native value tracing now establishes full transport parity.
|
||||
2. Real binary dispatch and nib → full product transports, with normalized
|
||||
semantic goldens. The compiler-backed readers already run in the new Apple
|
||||
CI lane; synthetic pointer/layout rejection tests remain useful.
|
||||
3. An approved interactive macOS runner for native UI capture/actions, including
|
||||
stdio MCP scenarios. Hosted runners without permissions cannot supply this
|
||||
proof. No arbitrary application's windows should be opened for this lane.
|
||||
4. Real macOS/Linux Ghidra CI runners with bring-your-own installations. This
|
||||
change adds Apple artifact CI without installing Java or Ghidra.
|
||||
5. Classify fake-runner replay/application acceptance accurately and replace
|
||||
repeated happy paths only after real sandbox journeys assert the same claims.
|
||||
The replay workflow's allow-unavailable mode must not imply executed replay.
|
||||
6. Replace repeated-array schema ceiling tests with real producer/transport
|
||||
regressions before deleting them. Existing integration tests do not yet
|
||||
establish their large-input guarantees.
|
||||
|
||||
Some static guards protect irreversible release authority rather than product
|
||||
behavior. Keep these few guards explicit and separate from E2E evidence.
|
||||
|
||||
## Verification after the expanded audit
|
||||
|
||||
- `npm run check:pr`: 351 files passed; 1,794 tests passed and two skipped.
|
||||
Typecheck, lint, formatting, unused-code and generated-document gates passed.
|
||||
- `npm run check:fast`: passed; 70 lint warnings and zero errors.
|
||||
- `npm run verify:browser`: actual Chromium observation and scenario CLI routes,
|
||||
retained artifacts, owned/external lifecycle checks and large CLI/stdio MCP PNG
|
||||
capture passed.
|
||||
- `npm run verify:ghidra`: macOS ARM64 with Ghidra 12.1.4/JDK 21 passed, including
|
||||
real native value tracing through CLI and stdio MCP: 171 nodes, 298 edges and
|
||||
six decompilations.
|
||||
- `npm run verify:asset-catalog`: real actool/assetutil → CLI and stdio MCP,
|
||||
complete records, pagination and malformed input checks passed.
|
||||
- `npm run verify:keyed-archive`: real Foundation binary/XML → CLI and stdio MCP,
|
||||
full graph golden, shared/cyclic identity and malformed scope checks passed.
|
||||
|
||||
- `npm run verify:fixtures`: seven source-built host fixtures, source/artifact
|
||||
digests and exhaustive inventory checks passed.
|
||||
|
||||
No Hopper windows were opened for this audit. The closing pass runs real Ghidra
|
||||
on macOS ARM64; earlier Hopper evidence remains separate. No new hosted CI success
|
||||
is claimed.
|
||||
+185
-37
@@ -1,8 +1,15 @@
|
||||
# Testing REA
|
||||
|
||||
REA's test suite follows one rule: use the lowest behavioral depth that can
|
||||
prove a claim. Tests are grouped into native Vitest projects so ownership,
|
||||
allowed dependencies, and runtime cost are visible from their paths.
|
||||
Prefer evidence in this order: full end-to-end workflows with real production
|
||||
providers, integration tests across data/API boundaries, then golden regressions
|
||||
from real captured inputs. Keep focused module tests for distinct failure or
|
||||
semantic cases that these workflows cannot reliably reproduce. A test's path
|
||||
or suite name does not establish its behavioral depth.
|
||||
|
||||
Tests are grouped into Vitest projects so ownership, allowed dependencies and
|
||||
runtime cost are visible from their paths. Before pruning a test, identify the
|
||||
replacement scenario and its assertions; passing success journeys do not
|
||||
replace malformed input, cancellation, permission or cleanup coverage.
|
||||
|
||||
## Behavioral depths
|
||||
|
||||
@@ -12,7 +19,7 @@ allowed dependencies, and runtime cost are visible from their paths.
|
||||
| Composition | `tests/composition/**` | Provider-neutral session and registry wiring; filesystem access is limited to fixture materialization |
|
||||
| Boundary | `tests/boundary/**` | Exactly one production filesystem, process, network, browser, CLI, or provider boundary |
|
||||
| MCP boundary | `tests/boundary/mcp/**` | MCP transport and tool-contract boundaries with isolated sessions and explicit cleanup |
|
||||
| Acceptance | `tests/acceptance/**` | A complete compiled CLI or MCP workflow |
|
||||
| Acceptance | `tests/acceptance/**` | A compiled CLI or MCP journey; injected providers still make it integration |
|
||||
| Conformance | `tests/conformance/**` | Shared provider contracts, parameterized by declared capabilities and explicit opt-outs |
|
||||
| Evaluation | `tests/evaluation/**` | Deterministic evaluator parsing, scoring, and report generation |
|
||||
|
||||
@@ -31,17 +38,55 @@ with the suite and excluded from package builds; broader runtime and provider
|
||||
fixtures remain under `tests/fixtures/**`.
|
||||
|
||||
`tests/process-global/**` is reserved for cases with a demonstrated dependency
|
||||
on process-global state. Those tests run without file parallelism. Reusable,
|
||||
on process-global state. Those tests use isolated forks so environment and
|
||||
exit-status changes cannot leak between files. Serialize a case only when it
|
||||
demonstrably shares an external resource that cannot be isolated. Reusable,
|
||||
test-scoped fixtures live under `tests/support/**`; immutable source artifacts
|
||||
remain under `tests/fixtures/**`.
|
||||
The process-global Vitest configuration contract rejects new direct temporary-root
|
||||
creation outside the workspace seam and its narrowly documented boundary/package
|
||||
exceptions.
|
||||
|
||||
Real Hopper, Ghidra, browser, package, managed-code, and controlled-replay
|
||||
claims belong to their explicit `npm run verify:*` lanes. They are not inferred
|
||||
from mocks or folded into the deterministic local gate. Real model trials are
|
||||
manual; Vitest covers only deterministic evaluator logic.
|
||||
Real Hopper, Ghidra, browser, package, and managed-code claims belong to their
|
||||
explicit `npm run verify:*` lanes. The reconstruction-readiness lane also
|
||||
checks deterministic rerun, tamper, and stale-input handling; those checks do
|
||||
not execute extracted JavaScript modules. When application runtime behavior is
|
||||
needed, exercise the actual target through browser, Electron, or process
|
||||
capture. Real model trials are manual; Vitest covers deterministic evaluator
|
||||
logic.
|
||||
|
||||
## End-to-end, integration and golden evidence
|
||||
|
||||
Full E2E tests invoke the production command dispatcher and real providers,
|
||||
without fake launchers, runners or responses. `verify:keyed-archive` writes an
|
||||
actual Foundation binary archive, runs the CLI and a separate stdio MCP
|
||||
subprocess, checks parity and pagination, rejects an escaping path and checks
|
||||
an XML graph golden. `verify:asset-catalog` compiles source-owned colors with
|
||||
`actool`, invokes real `assetutil`, then checks CLI/MCP results, exact catalog
|
||||
digest, every raw metadata field, pagination and malformed input rejection.
|
||||
Neither artifact workflow requires Hopper or launches it. Both run in macOS CI.
|
||||
|
||||
MCP SDK transport tests with recording providers remain integration tests.
|
||||
They are useful for schema drift and failure projection but do not prove that
|
||||
Hopper, Ghidra or another substituted engine works. `verify:package` proves
|
||||
packaging/install behavior and fake-provider integration; use the corresponding
|
||||
real-provider lanes for engine claims. Real Apple dispatch and Interface
|
||||
Builder verifiers currently prove format integration through production readers.
|
||||
|
||||
Golden tests use immutable captured text inputs with producer/source provenance
|
||||
under `tests/fixtures/golden/`. Expected results are reviewed for the semantic
|
||||
claim; capture commands do not automatically approve new expected outputs.
|
||||
Do not call handcrafted utility output or synthetic binary builders real-data
|
||||
goldens. Keep unsupported binary layouts and malformed boundaries as targeted
|
||||
regressions until a real fixture establishes equivalent coverage.
|
||||
|
||||
`verify:browser` also captures a source-owned noise canvas as a real PNG above
|
||||
8 MiB through the CLI and stdio MCP, with complete byte/digest parity and real PNG
|
||||
decoding. Its SDK client explicitly permits the larger inline JSON response;
|
||||
this lane does not establish large image-comparison request transport coverage.
|
||||
|
||||
The [test suite audit](test-suite-audit.md) records the pruning decisions,
|
||||
replacement evidence and remaining priorities.
|
||||
|
||||
## Real-toolchain verification lanes
|
||||
|
||||
@@ -54,16 +99,74 @@ that a host or target is covered when it was skipped.
|
||||
|
||||
| Ghidra lane | Supported runner/target | Additional local tools |
|
||||
| ------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| `npm run verify:ghidra` | Linux x64 with x86-64 ELF | Host C compiler, Ghidra 12.1.4, and full JDK 21 |
|
||||
| `npm run verify:ghidra:aarch64-jump-table` | Any supported Ghidra host; AArch64 Linux ELF relocatable fixture | Clang with AArch64 target support, Ghidra 12.1.4, and full JDK 21 |
|
||||
| `npm run verify:ghidra` | Linux x64 ELF or macOS x64/arm64 Mach-O | Host C compiler, Ghidra 12.1.4, and full JDK 21 |
|
||||
| `npm run verify:ghidra:switch` | Linux x64 ELF; GCC/Clang optimized and stripped switch fixtures | GCC, Clang, GNU nm/objdump/strip, Ghidra 12.1.4, and full JDK 21 |
|
||||
| `npm run verify:ghidra:aarch64-jump-table` | Any supported Ghidra host; AArch64 ELF; byte/halfword tables; host ARM64 Mach-O | Clang with AArch64 target support, Ghidra 12.1.4, and full JDK 21 |
|
||||
| `npm run verify:ghidra:cross-format` | Any supported Ghidra host; also analyzes AArch64 ELF, x86-64 PE, and x86-64 Mach-O | `clang`, LLD, and `lld-link` in addition to host-lane prerequisites |
|
||||
| `npm run verify:ghidra:windows` | Controlled Windows x64 with native x86-64 PE | Ghidra 12.1.4, full JDK 21, and the Windows P0 fixture toolchain |
|
||||
| `npm run verify:ghidra:windows` | Controlled Windows x64 with native x86-64 PE | Ghidra 12.1.4, full JDK 21, and the matching native artifact |
|
||||
|
||||
Windows native conformance runs with `npm run verify:windows-native` and does
|
||||
not require Ghidra or Java. An optional independently compiled Windows fixture
|
||||
adds in-place reparse, breakaway, environment, and token observations.
|
||||
`npm run verify:ghidra:windows:package` additionally packs and installs REA into
|
||||
an isolated prefix and runs ordinary-user CLI/MCP operations against their
|
||||
canonical schemas. Run the controlled fixture generator before that lane, or
|
||||
supply an installed package root and an explicit fixture as arguments.
|
||||
The host-native Ghidra lane also verifies native value tracing through the
|
||||
production CLI and a separate stdio MCP process. It compares complete dependency
|
||||
graphs, validates Evidence and upstream/workflow profiles, checks capability
|
||||
discovery, and closes the MCP session. No provider or transport is mocked.
|
||||
|
||||
The Linux switch lane checks dense, sparse-with-holes, shared-body, nonzero,
|
||||
negative, and nonexact JSON integer labels plus a comparison-only control.
|
||||
Independent source labels, ELF file bytes, table slots, and bounds branches
|
||||
define expected case/default destinations. Production CLI and MCP must agree;
|
||||
debug labels must retain their signed values. For stripped negative fixtures,
|
||||
the independently checked 32-bit dispatch permits equivalent unsigned labels
|
||||
only alongside the reported low-confidence `undefined4` parameter; original
|
||||
source signedness remains unknown and the ABI residual must remain visible.
|
||||
unsafe labels remain unresolved and every recovered destination is retained.
|
||||
The compiler oracle also injects malformed records to check that its assertions
|
||||
reject missing/default-confused labels, wrong destinations, and numeric guesses.
|
||||
It also invokes the actual bridge methods on detached Ghidra model objects to
|
||||
check ambiguous dispatches, signed literals, precision bounds, shared targets,
|
||||
and conflicting labels. This reflection fixture depends on the pinned Ghidra
|
||||
model and does not claim a compiler produced those synthetic graph shapes.
|
||||
Pass `--entrypoint /path/to/installed/rea-agents/scripts/rea.mjs` directly to
|
||||
`scripts/verify-real-ghidra-switch.mjs` to verify an installed package through
|
||||
the same compiler oracles and CLI/MCP checks.
|
||||
|
||||
The cross-format Ghidra lane also analyzes an optimized AArch64 ELF switch
|
||||
fixture. It checks the recovered case values against the source cases and
|
||||
requires unresolved table bounds or case mappings to remain visible as
|
||||
residual unknowns.
|
||||
|
||||
`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
|
||||
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.
|
||||
|
||||
## DOS Ghidra analysis
|
||||
|
||||
`npm run verify:ghidra:dos` requires the supported Ghidra and JDK installation
|
||||
on Linux x64 or macOS x64/arm64. It generates a source-owned MZ fixture without
|
||||
a DOS emulator or compiler, then checks real 16-bit decoding, segment
|
||||
relocation, near/far calls, decompilation, disjoint function body ranges,
|
||||
stable CLI/MCP observations, unchanged source bytes, and owned process/project
|
||||
cleanup. Raw p-code address-space selector tokens are reported separately from
|
||||
the stable observation comparison. Linux x64 is verified; macOS DOS remains
|
||||
unverified. This lane is separate from host-native and optional cross-format
|
||||
verification. See [DOS analysis](ghidra-dos.md).
|
||||
|
||||
`npm run verify:ghidra:com` uses a generated headerless fixture with no compiler,
|
||||
DOS emulator or game data. It exercises explicit admission, BinaryLoader entry
|
||||
preparation, measured register context, whole-file byte readback, source offsets,
|
||||
unmapped PSP/partial reads, actual decompilation, CLI/MCP parity and owned cleanup.
|
||||
It has the same Ghidra/JDK prerequisites as the MZ lane. Neither lane claims DOS
|
||||
runtime or PC-98 device execution.
|
||||
|
||||
## Apple Interface Builder archives
|
||||
|
||||
`npm run verify:interface-builder` compiles the source-owned AppKit XIB into a
|
||||
@@ -77,30 +180,61 @@ not make native host acceptance unavailable.
|
||||
|
||||
## Developer commands
|
||||
|
||||
`npm test` runs every deterministic project once. The narrower feedback loops
|
||||
are:
|
||||
Use source feedback while editing, explicit boundary checks for the changed
|
||||
behavior, and complete CI evidence before merging.
|
||||
|
||||
| Command | Scope |
|
||||
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| `npm run test:fast` | Domain, service, adapter, composition, boundary, MCP boundary, and conformance projects |
|
||||
| `npm run test:local` | Changed and related tests without the build step; lightweight local feedback loop |
|
||||
| `npm run test:boundary` | Boundary, MCP boundary, and process-global projects |
|
||||
| `npm run test:mcp` | MCP boundary project only |
|
||||
| `npm run test:acceptance` | Complete CLI and MCP acceptance workflows |
|
||||
| `npm run test:changed` | Changed tests in non-serial projects via Vitest's import graph |
|
||||
| `npm run test:watch` | Changed domain, service, adapter, composition, boundary, MCP boundary, conformance, and evaluation tests in watch mode |
|
||||
| `npm run test:watch:all` | Changed tests from every deterministic project in watch mode |
|
||||
| `npm run check:changed` | Cached static checks followed by changed tests |
|
||||
| `npm run check:pr` | Static, generated-document, and complete deterministic PR gate |
|
||||
| Command | Scope |
|
||||
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| `npm run test:local` | Dirty source tests via the import graph, without build; explicit source paths run regardless of Git status |
|
||||
| `npm run test:focused -- PATH...` | Exact existing test files; compiled boundaries build first, and unmatched paths fail |
|
||||
| `npm run test:changed` | Source tests affected since the merge base with `origin/main`, including committed and dirty changes |
|
||||
| `npm run test:fast` | All domain, service, adapter, composition, conformance, and evaluation tests without build |
|
||||
| `npm run test:boundary` | Boundary, MCP boundary, process boundary, and process-global projects |
|
||||
| `npm run test:mcp` | MCP boundary project |
|
||||
| `npm run test:acceptance` | Complete compiled CLI and MCP acceptance workflows |
|
||||
| `npm run test:watch` | Dirty source tests in watch mode, without build |
|
||||
| `npm run test:watch:all` | Changed tests from every project; builds at startup, so rebuild after production edits before relying on compiled tests |
|
||||
| `npm run check:changed` | Cached typecheck/lint and branch-related source feedback |
|
||||
| `npm run check:pr` | Opt-in complete local deterministic gate and generated-file checks |
|
||||
| `npm run docs:check` | Committed generated-document freshness |
|
||||
|
||||
Changed-test selection is a fast feedback aid, not release evidence. It can
|
||||
miss behavior connected through runtime registration, generated data, shell
|
||||
entrypoints, or other relationships that are absent from the import graph.
|
||||
Use `npm run check:pr` before handing off a contribution.
|
||||
For example:
|
||||
|
||||
Local full-suite Vitest runs are intentionally capped at one worker and one
|
||||
project at a time. Boundary fixtures own real subprocesses, and this local cap
|
||||
keeps aggregate memory predictable; CI retains the existing two-worker budget.
|
||||
```bash
|
||||
npm run test:local -- src/config.test.ts
|
||||
npm run test:focused -- tests/acceptance/applications/runtime.test.ts
|
||||
npm run test:changed -- --base origin/main
|
||||
npm run test:changed -- --dry-run
|
||||
```
|
||||
|
||||
`test:focused` accepts exact repository-relative test file paths. Source-only
|
||||
paths do not build; boundary, acceptance, process-global, or unfamiliar `tests/`
|
||||
paths build conservatively. Tests use Vitest concurrency and isolated
|
||||
workspaces; commands do not hold a broad test lock. Build and documentation
|
||||
writers retain checkout-local locks for their shared output files. Explicit
|
||||
selections do not use `--changed` or permit zero-test success.
|
||||
The dry-run option reports the chosen merge base, scope and build prerequisite
|
||||
without executing tests or building. A missing Git base reports how to fetch
|
||||
it or select another revision.
|
||||
|
||||
Changed selection can miss runtime registration, generated data, shell
|
||||
entrypoints, bridges, or other relationships absent from the import graph.
|
||||
An empty changed selection means no tests were selected, not verified
|
||||
correctness. Select relevant boundary files and real-provider lanes explicitly.
|
||||
Source projects also contain large capacity regressions; `test:fast` promises
|
||||
no compiled-runtime prerequisite, not a fixed time budget.
|
||||
|
||||
Routine iterations and rebases need focused regressions and relevant checks.
|
||||
Before handing off a PR, run `npm run check` and generated-document checks when
|
||||
applicable; CI owns the full suite and coverage. Use the full local gate for
|
||||
broad changes or diagnosing CI, rather than after every edit. Package/install
|
||||
changes additionally need package verification; provider changes need actual
|
||||
provider evidence.
|
||||
|
||||
Local full-suite Vitest runs use up to two workers and schedule projects one
|
||||
at a time. Process, acceptance and process-global projects serialize their
|
||||
files to prevent competing lifecycle observations. CI retains its two-worker
|
||||
budget.
|
||||
The pure domain/contracts and recording-port service projects share one worker
|
||||
module context because their tests own no mutable runtime resources. MCP
|
||||
boundary files also share the immutable server module graph while creating and
|
||||
@@ -109,8 +243,7 @@ process-global, and other boundary projects retain per-file isolation.
|
||||
`npm test`, `npm run docs:check`, and `npm run docs:generate` share
|
||||
repository-local locks and fail fast when the same class of command is already
|
||||
running. The `npm test` build is inside that lock. `check:pr` runs its test task
|
||||
before starting documentation validation, so TypeDoc does not compete with the
|
||||
full suite for memory.
|
||||
before starting generated-document validation.
|
||||
|
||||
Vitest and Node persistent compile caches are deliberately not enabled by
|
||||
default. To evaluate repeated local runs, opt in for both cold and warm
|
||||
@@ -144,5 +277,20 @@ for another.
|
||||
The PR acceptance target is a median `npm run check:pr` wall time below three
|
||||
minutes across three warm-build runs on the benchmark host. Keep Vitest caches
|
||||
cold unless separately identified. A PR that touches packaging or real-system
|
||||
behavior also requires `npm run verify:package` and the applicable
|
||||
`verify:*` lanes.
|
||||
behavior requires the applicable `verify:*` lanes; packaging and installation
|
||||
changes also require `npm run verify:package`. The full-gate benchmark measures
|
||||
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.
|
||||
|
||||
+10
-9
@@ -8,14 +8,14 @@ protocol, authority, and resource-safety constraints, and report their effects.
|
||||
|
||||
## Choose the tool shape
|
||||
|
||||
| Shape | Use it for | Contract should return |
|
||||
| -------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| `inspect` | Facts about one explicit target, address, object, or resource | Relevant fields, source locations, and facet-level availability |
|
||||
| `search` / `list` | Finding candidate targets or entities | Stable ordering and useful result context; expose pagination when the result format or caller's query needs it |
|
||||
| `trace` | Relationships across code, metadata, UI resources, or observations | Typed edges, supporting evidence, and unresolved paths; identify any genuine traversal boundary |
|
||||
| `compare` | Two explicitly identified artifacts, versions, or evidence sets | Paired identity, comparable coverage, and deltas with evidence |
|
||||
| `workflow` | A distinct analyst outcome that benefits from composition inside REA | A useful inline result, contributing evidence, and partial/unavailable facets |
|
||||
| `observe` / `capture` / `replay` | A question that requires runtime behavior | Required authority, launch/attach behavior, real operational constraints, lifecycle, and cleanup status |
|
||||
| Shape | Use it for | Contract should return |
|
||||
| --------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| `inspect` | Facts about one explicit target, address, object, or resource | Relevant fields, source locations, and facet-level availability |
|
||||
| `search` / `list` | Finding candidate targets or entities | Stable ordering and useful result context; expose pagination when the result format or caller's query needs it |
|
||||
| `trace` | Relationships across code, metadata, UI resources, or observations | Typed edges, supporting evidence, and unresolved paths; identify any genuine traversal boundary |
|
||||
| `compare` | Two explicitly identified artifacts, versions, or evidence sets | Paired identity, comparable coverage, and deltas with evidence |
|
||||
| `workflow` | A distinct analyst outcome that benefits from composition inside REA | A useful inline result, contributing evidence, and partial/unavailable facets |
|
||||
| `observe` / `capture` | A question that requires runtime behavior | Required authority, launch/attach behavior, real operational constraints, lifecycle, and cleanup status |
|
||||
|
||||
These are task shapes, not required prefixes. Name a tool for the action and
|
||||
object agents reason about; keep the name distinct from nearby alternatives.
|
||||
@@ -72,7 +72,8 @@ or model-authored loop when one call can answer the question.
|
||||
- Separate observed facts from derived and inferred edges. Cite the evidence
|
||||
supporting every important relationship; preserve unresolved edges.
|
||||
- Declare read-only, mutation, process, filesystem, network, and UI effects
|
||||
truthfully in the contract and permission boundary.
|
||||
truthfully in the contract and enforce the target and lifecycle named in the
|
||||
request.
|
||||
- Keep provider-specific types out of provider-neutral domain and application
|
||||
layers. Normalize supported provider results without implying equal coverage.
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
|
||||
45.5,src/application/Uninstall.ts:removeClient restore,Configuration update fails and restoring the original also fails,"Configuration could not be updated or restored. Restore its `.rea.backup` manually, then rerun uninstall.","High risk; persistent configuration may be damaged, but retained backup is authoritative",Keep new recovery copy,implemented,verified by injected restoration failure test
|
||||
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/Doctor.ts:node,Runtime version is outside supported ranges,Install Node.js 22.19+ or 24.11+.,Low risk; exact dependency recovery,Keep current copy,unchanged,verified by doctor unit 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
|
||||
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
|
||||
@@ -65,7 +65,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
|
||||
55,src/application/Setup.ts:optional engine,Setup completes without optional deep-analysis engine,Hopper is optional for non-Hopper providers. Rerun with --yes --install-hopper for deep native analysis.,Exposes provider terminology but this is an explanatory setup status rather than an error; recovery command is exact,Keep current copy,unchanged,verified by setup unit test
|
||||
56,src/application/Setup.ts:doctor fallback,Post-setup diagnostics remain unhealthy,Run rea doctor and apply each reported remediation.,Low risk; delegates to structured per-check recovery,Keep current copy,unchanged,verified by exact setup unit test
|
||||
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/Setup.ts:unsupported Node,Runtime version cannot run setup,Install Node.js 22.19+ or 24.11+ and rerun setup.,Low risk; exact dependency and retry,Keep current copy,unchanged,verified by setup host tests
|
||||
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
|
||||
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
|
||||
@@ -83,11 +83,11 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
|
||||
74,install.sh:unknown option,Installer receives an unsupported flag,unknown option with caller-provided flag,Low risk; identifies offending input,Keep current copy,unchanged,verified by shell branch inspection
|
||||
75,install.sh:platform,Installer runs outside macOS or supported Linux,REA supports macOS and selected 64-bit Linux distributions.,Low risk; states support boundary,Keep current copy,unchanged,verified by shell branch inspection
|
||||
76,install.sh:curl missing,curl is absent,curl is required.,No recovery instruction,"curl is required. Install curl, then rerun this installer.",implemented,verified by exact missing-curl installer test
|
||||
77,install.sh:node missing,node is absent,Node.js 22.19+ or 24.11+ is required; install it from nodejs.org and retry.,Low risk; exact versions and recovery,Keep current copy,unchanged,verified by shell branch inspection
|
||||
77,install.sh:node missing,node is absent,"Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from nodejs.org and retry.",Low risk; exact versions and recovery,Keep current copy,unchanged,verified by shell branch inspection
|
||||
78,install.sh:npm missing,npm is absent,npm is required; install it with Node.js and retry.,Low risk; exact recovery,Keep current copy,unchanged,verified by shell branch inspection
|
||||
79,install.sh:node unreadable,node exists but does not report a parseable version,could not determine the active Node.js version.,No cause-local recovery,"The active Node.js version could not be read. Check that node works and is on PATH, then retry.",implemented,verified by exact unreadable-Node-version installer test
|
||||
80,install.sh:Node 22 floor,Node 22 minor version is below 19,Node.js VERSION is unsupported; use Node.js 22.19+ or 24.11+.,Low risk; exact detected version and supported alternatives,Keep current copy,unchanged,verified by exact installer test using Node 20 and branch inspection
|
||||
81,install.sh:Node 24 floor,Node 24 minor version is below 11,Node.js VERSION is unsupported; use Node.js 24.11 or newer.,Low risk; exact detected version and floor,Keep current copy,unchanged,verified by shell branch inspection
|
||||
80,install.sh:Node 22 floor,Node 22 minor version is below 19,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact detected version and supported alternatives,Keep current copy,unchanged,verified by exact installer test using Node 20 and branch inspection
|
||||
81,install.sh:Node 24 floor,Node 24 minor version is below 11,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact detected version and floor,Keep current copy,unchanged,verified by shell branch inspection
|
||||
82,install.sh:version syntax,Explicit version is not exact semantic version,version must be an exact semantic version.,Low risk; exact correction,Keep current copy,unchanged,verified by shell branch inspection
|
||||
83,install.sh:release lookup,Latest GitHub release request fails,could not resolve the latest GitHub release.,No fallback despite supporting pinned versions,"The latest REA release could not be resolved. Check network access or pass --version VERSION, then retry.",implemented,verified by exact release-lookup installer test
|
||||
84,install.sh:release response,Latest release response is not valid expected JSON,Node JSON parser stack followed by the safe release-response message,Leaks a parser stack and buries recovery guidance,The release response was invalid. Retry later or pass --version VERSION.,implemented,verified by exact malformed-release test with parser stderr suppressed
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"ledger_id": "ecl_b01c0b28362b48128765d176113471a5a82637fca0acfcd3182971155942e0d4",
|
||||
"ledger_id": "ecl_132e857a8a93f0b366cd72f9cfa1c338ffd4100460a7834ef69abff936c84472",
|
||||
"records": [
|
||||
{
|
||||
"claim_id": "managed.application-graph-projection",
|
||||
@@ -78,13 +78,6 @@
|
||||
"ev_f974f4b9d039cda841894c6173cae86427ecef48f4e7a4d6c73edf7b23af2b2a"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.runtime-correlation-admission-plan",
|
||||
"status": "pass",
|
||||
"evidence_ids": [
|
||||
"ev_3e546c20493faf9696502171a5cfe9504569cffb22f0561ee3000f0027b9bbd8"
|
||||
]
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.unicode-obfuscated-identifiers",
|
||||
"status": "pass",
|
||||
@@ -101,8 +94,8 @@
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total": 14,
|
||||
"pass": 12,
|
||||
"total": 13,
|
||||
"pass": 11,
|
||||
"fail": 0,
|
||||
"unsupported": 2,
|
||||
"truncated": 0,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"manifest_id": "ecm_26db97ee1f42255040febd543bb2f3feac773f48d5ef38bd6651cd8770b36013",
|
||||
"manifest_id": "ecm_125743de92b0cbb08a3cb11ee0d9305f60e5c4fb81152b8b748d348f91f99ee8",
|
||||
"verifier": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
@@ -13,7 +13,7 @@
|
||||
"skill_digests": [
|
||||
{
|
||||
"skill_id": "reverse-engineer-anything",
|
||||
"sha256": "28122d8fd448db430249431aedfa9d3d865da8a323b703631fec5c44041d9d77"
|
||||
"sha256": "26338313efd5c941e1f3e1f1d35e2605d47749f674c359458e76d5a470b0cbb1"
|
||||
}
|
||||
],
|
||||
"claims": [
|
||||
@@ -169,20 +169,6 @@
|
||||
"version": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.runtime-correlation-admission-plan",
|
||||
"scenario": {
|
||||
"id": "runtime-correlation-admission-plan",
|
||||
"version": 1
|
||||
},
|
||||
"artifact_sha256s": [
|
||||
"cead4cc99733e4e56a5fd0cec8a0813bbc24e249aa72a10eca4c47ee3b0e129d"
|
||||
],
|
||||
"provider": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"claim_id": "managed.unicode-obfuscated-identifiers",
|
||||
"scenario": {
|
||||
|
||||
+109
-100
@@ -1,156 +1,165 @@
|
||||
# Windows Ghidra P0
|
||||
|
||||
Status: experimental. This boundary is useful for approved local fixtures, but
|
||||
it is not general Windows REA support and must not be used for hostile,
|
||||
sensitive, security-decision, or concurrently mutable targets.
|
||||
Status: experimental Windows x64 support for the read-only P0 boundary. The
|
||||
published package bundles native process, filesystem, and DACL controls. REA
|
||||
uses them automatically when the analyst selects a Ghidra operation; no
|
||||
additional permission flag or degraded mode is required.
|
||||
|
||||
## Supported boundary
|
||||
|
||||
The P0 accepts exactly:
|
||||
|
||||
- a Windows x64 host;
|
||||
- Node.js 22.19+ or 24.11+;
|
||||
- Windows 10+ x64 with fixed local NTFS target and temporary volumes;
|
||||
- Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+;
|
||||
- an operator-installed official Ghidra 12.1.4 distribution;
|
||||
- a 64-bit full JDK 21;
|
||||
- an explicit native, non-managed, non-DLL x86-64 PE application; and
|
||||
- the existing 19 read-only Ghidra inventory and function-analysis operations.
|
||||
- the 25 read-only Ghidra inventory, memory, and function-analysis operations.
|
||||
|
||||
Hopper, Ghidra GUI state, mutation, controlled JavaScript replay, process
|
||||
capture, browser/Electron observation, artifact extraction, and general
|
||||
Windows feature parity are not implied by this boundary. Managed PE/CLI
|
||||
inspection remains a separate execution-free provider and is not routed
|
||||
through Windows Ghidra P0.
|
||||
Loaded memory reads and file offsets preserve Ghidra's observed source mappings.
|
||||
PE load-image inspection returns measurements with an explicit unsupported
|
||||
attestation status; independent format-specific verification currently supports
|
||||
DOS MZ only.
|
||||
|
||||
REA does not install or upgrade Ghidra, Java, Python, npm, Node.js, Hopper, or
|
||||
another package on Windows. The production adapter uses the packaged Java
|
||||
`HeadlessScript`; Python and PyGhidra are not prerequisites.
|
||||
This boundary does not establish general Windows feature parity. Ghidra GUI
|
||||
state, annotations, other target architectures and formats, and Hopper-only
|
||||
operations remain unavailable through this provider. Managed PE/CLI inspection
|
||||
uses its separate execution-free provider.
|
||||
|
||||
REA does not install or upgrade Ghidra, Java, Python, npm, Node.js, Hopper, or a
|
||||
compiler. The adapter uses the packaged Java `HeadlessScript`; Python and
|
||||
PyGhidra are not prerequisites. Users do not build the native addon.
|
||||
|
||||
## Configuration
|
||||
|
||||
Install REA, Ghidra, and JDK 21 separately. In PowerShell:
|
||||
|
||||
```powershell
|
||||
npm install --global rea-agents
|
||||
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
|
||||
$env:JAVA_HOME = "C:\Program Files\Java\jdk-21"
|
||||
$env:JAVA_HOME = "C:\tools\jdk-21"
|
||||
$env:REA_ANALYSIS_PROVIDER = "ghidra"
|
||||
|
||||
rea doctor --json
|
||||
rea providers --json
|
||||
rea inspect "C:\approved-fixtures\sample.exe" --provider ghidra --format json
|
||||
rea inspect "C:\fixtures\sample.exe" --provider ghidra --format json
|
||||
```
|
||||
|
||||
`rea doctor` checks the x64 host, exact Ghidra release,
|
||||
`support\analyzeHeadless.bat`, `java.exe`, `javac.exe`, JDK bitness, and JDK
|
||||
major version. `rea setup` remains unavailable on Windows and makes no changes.
|
||||
`rea doctor` distinguishes installation prerequisites from native artifact or
|
||||
OS failures. It checks the Ghidra release, headless launcher, full JDK, x64
|
||||
host, and native controls. A missing or mismatched addon requires the matching
|
||||
Windows x64 package; changing MCP registration cannot supply that artifact.
|
||||
A filesystem or access failure identifies the failed native constraint.
|
||||
Other doctor checks, including unconfigured agent integrations, can remain
|
||||
unready while Ghidra itself is available.
|
||||
|
||||
For an MCP client, register the resolved Node entry point and preserve the
|
||||
three environment variables above. A representative configuration is:
|
||||
`rea setup` can register supported agent integrations and the bundled skill.
|
||||
Its existing preview and approval apply to configuration writes. Setup
|
||||
preserves valid Ghidra and Java settings even when a separate runtime
|
||||
prerequisite is unavailable.
|
||||
|
||||
For MCP, register the resolved Node entry point and the same provider settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"rea-windows-ghidra-p0": {
|
||||
"command": "C:\\Program Files\\nodejs\\node.exe",
|
||||
"args": [
|
||||
"C:\\Users\\analyst\\AppData\\Roaming\\npm\\node_modules\\rea-agents\\scripts\\rea.mjs",
|
||||
"mcp"
|
||||
],
|
||||
"rea": {
|
||||
"command": "C:\\tools\\node\\node.exe",
|
||||
"args": ["C:\\tools\\rea\\scripts\\rea.mjs", "mcp"],
|
||||
"env": {
|
||||
"REA_ANALYSIS_PROVIDER": "ghidra",
|
||||
"GHIDRA_INSTALL_DIR": "C:\\tools\\ghidra_12.1.4_PUBLIC",
|
||||
"JAVA_HOME": "C:\\Program Files\\Java\\jdk-21"
|
||||
"JAVA_HOME": "C:\\tools\\jdk-21"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Resolve the actual global package path on the intended machine; do not copy the
|
||||
example path blindly. Restart the MCP client after changing its registration.
|
||||
These paths are examples. Use the actual package location and restart the MCP
|
||||
client after registration changes. Cold Ghidra import can exceed a client's
|
||||
short default request timeout; the real verification lane allows the existing
|
||||
330-second provider startup deadline to complete.
|
||||
|
||||
## Target and import identity
|
||||
## Native authority and target identity
|
||||
|
||||
PE parsing records architecture, COFF executable role, and whether the CLI
|
||||
header data-directory entry is non-empty. Header classification and SHA-256
|
||||
are read from the same open file handle. Windows admission then rejects:
|
||||
The verified boundary excludes SUBST aliases and changing drive-letter/DOS-device
|
||||
namespaces. REA does not claim a separate SUBST detector. Mounted-folder entries
|
||||
that expose reparse tags fail component admission; broader namespace variants
|
||||
have not been established by this lane. Requested paths, final handle paths,
|
||||
volume serials, and file IDs remain separate observations.
|
||||
|
||||
- non-PE formats;
|
||||
- x86, ARM, ARM64, or unknown architectures;
|
||||
- DLL/shared-library and non-executable image roles;
|
||||
- managed or incompletely classified PE targets; and
|
||||
- malformed optional headers or data-directory declarations.
|
||||
The loader validates package version, ABI, Node-API compatibility, PE machine,
|
||||
and artifact SHA-256 before loading the package-owned addon. Native failure
|
||||
reasons remain distinct from a valid installation and an unsupported target.
|
||||
|
||||
After admission, REA copies the target into the ephemeral Ghidra runtime and
|
||||
requires the snapshot digest to match the admitted SHA-256. Ghidra imports only
|
||||
that snapshot. Before serving a request, the Java bridge requires Ghidra's
|
||||
`Program.getExecutableSHA256()` observation to match the same digest. A path
|
||||
replacement that changes bytes therefore fails before Evidence is created.
|
||||
PE header classification and SHA-256 come from the same open file. The Windows
|
||||
provider admits native x86-64 PE applications and rejects unsupported roles,
|
||||
architectures, managed images, and malformed headers. The original selected
|
||||
source coordinate is preserved for native admission.
|
||||
|
||||
This linkage does not establish Windows reparse-point or hostile-path safety.
|
||||
The current implementation has no handle-based `CreateFileW` authority backend
|
||||
and no verified no-follow equivalent for every path component.
|
||||
Admission opens and checks every path component without following a reparse
|
||||
point. It retains handles, verifies local NTFS semantics, and observes volume
|
||||
serial, 128-bit file ID, final handle path, requested path, and size. Reparse
|
||||
paths, alternate streams, ambiguous coordinates, UNC paths, and other
|
||||
filesystems are rejected with the actual constraint.
|
||||
|
||||
## Local transport and lifecycle
|
||||
The snapshot streams from the admitted source handle on a background worker.
|
||||
Its digest must match the parsed target digest. Ghidra imports that private,
|
||||
immutable snapshot, and the bridge requires the imported program SHA-256 to
|
||||
match before serving operations. Source and snapshot identities remain
|
||||
separate diagnostic observations. Snapshot cancellation is checked between
|
||||
64 KiB reads and cleanup waits for worker settlement.
|
||||
|
||||
Linux continues to use a mode-restricted Unix socket. Windows uses a Java
|
||||
listener bound to `127.0.0.1` on an ephemeral port. The bridge atomically
|
||||
publishes a bounded endpoint record containing only the literal loopback host
|
||||
and port. The random 256-bit request token remains only in the
|
||||
ephemeral session descriptor and every request is authenticated.
|
||||
## Private runtime, transport, and lifecycle
|
||||
|
||||
`analyzeHeadless.bat` is invoked through an absolute `cmd.exe` with a fixed
|
||||
argument sequence, command extensions enabled for the Ghidra script, delayed
|
||||
expansion disabled, and Node shell execution disabled. Every batch token is
|
||||
quoted. Paths containing command-interpreter metacharacters are rejected
|
||||
instead of escaped heuristically.
|
||||
Runtime creation installs a protected DACL permitting exactly the current user
|
||||
and SYSTEM. Readback checks the actual owner and ACEs by handle. Descriptors
|
||||
and snapshots remain locked until owner cleanup. Child files inherit the
|
||||
private boundary. Analysis runs with the caller's authority; these controls
|
||||
are not a sandbox for the Ghidra engine or other processes using the same user.
|
||||
|
||||
Close, cancellation, timeout, protocol failure, or provider exit removes the
|
||||
project and runtime root. Windows P0 uses bounded `taskkill /T /F` process-tree
|
||||
termination. It does not assign Ghidra to a Job Object and cannot make the same
|
||||
PID-reuse and descendant-ownership claim as the POSIX process-group backend.
|
||||
Windows uses an authenticated listener bound to `127.0.0.1` on an ephemeral
|
||||
port. The endpoint record contains only the literal loopback host and port.
|
||||
The bridge writes a new record and closes its writer before the native reader
|
||||
can admit it. The reader refuses concurrent writers and verifies its opened
|
||||
object's private DACL. The random 256-bit bearer token stays in the private,
|
||||
immutable session descriptor and is redacted from diagnostic output.
|
||||
|
||||
## Known security gates
|
||||
The batch launcher uses an absolute `cmd.exe`, fixed switches, delayed
|
||||
expansion disabled, and quoted tokens. Command-interpreter metacharacters in
|
||||
launch paths are unsupported. JVM home and temporary properties use quoted
|
||||
`JDK_JAVA_OPTIONS`, preserving spaces. `APPDATA` and `LOCALAPPDATA` point into
|
||||
the private runtime, isolating Ghidra preferences, logs, and caches.
|
||||
|
||||
The following remain explicitly unavailable:
|
||||
|
||||
- verified current-user-only DACLs for runtime directories, descriptors, logs,
|
||||
snapshots, and endpoint records;
|
||||
- a DACL-protected Windows named pipe or equivalent OS-identity-restricted IPC;
|
||||
- reparse-point-safe, handle-based input and output authority;
|
||||
- Job Object assignment before child execution and kill-on-close proof;
|
||||
- general extraction containment and filesystem-sensitive workflows; and
|
||||
- independent security acceptance for hostile or mutable targets.
|
||||
|
||||
Successful symlink creation or ConPTY availability does not satisfy any of
|
||||
these gates. Inspect the machine-readable report with:
|
||||
|
||||
```powershell
|
||||
npm run probe:windows-capabilities
|
||||
```
|
||||
Process creation assigns a suspended child atomically to an owned Job Object.
|
||||
Membership and kill-on-owner-close policy are verified before resume.
|
||||
Breakaway is disabled. Only the child's default file owner is normalized;
|
||||
the caller token stays unchanged. Caller CPU affinity and priority are
|
||||
preserved. Close, cancellation, timeout, startup failure, and provider failure
|
||||
terminate the job and verify all members have settled before runtime cleanup.
|
||||
Cleanup removes admitted objects by handle and unlinks reparse entries without
|
||||
following their targets. Abrupt owner death kills the job but can leave
|
||||
private runtime files; crash-time transactional deletion is not claimed.
|
||||
|
||||
## Verification
|
||||
|
||||
Hosted CI builds, type-checks, runs the curated Windows tests, records the named
|
||||
host capability report, packs the real npm artifact, installs it in isolation,
|
||||
and compares the packaged MCP catalog with `TOOL_CONTRACTS`.
|
||||
|
||||
The controlled real-engine lane is intentionally separate:
|
||||
See [native controls](../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.
|
||||
|
||||
```powershell
|
||||
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
|
||||
$env:JAVA_HOME = "C:\Program Files\Java\jdk-21"
|
||||
npm ci
|
||||
npm run verify:windows-native
|
||||
npm run verify:ghidra:windows
|
||||
npm run verify:ghidra:windows:package -- C:\fixtures\installed-rea C:\fixtures\sample.exe
|
||||
```
|
||||
|
||||
The verifier creates an ignored deterministic x86-64 PE fixture, checks its
|
||||
fixed SHA-256, opens Ghidra 12.1.4 through the production Java bridge, exercises
|
||||
all 19 admitted operations, verifies the target/snapshot/import digest chain,
|
||||
and checks endpoint, project, process, and runtime cleanup. The GitHub workflow
|
||||
accepts only the fixed `real-ghidra-windows` repository-dispatch event against
|
||||
default-branch `main`, a protected `real-ghidra-windows` environment, and a
|
||||
self-hosted runner labelled `Windows`, `x64`, and `ghidra-12-1-2`.
|
||||
The package lane must run with an ordinary, non-elevated user token. It exercises
|
||||
CLI inspection and every admitted MCP operation, validates named canonical
|
||||
input/output contracts, checks the source digest stays unchanged, and verifies
|
||||
runtime cleanup. Use a source-owned fixture, one analysis process at a time,
|
||||
and a bounded heap for real-provider verification.
|
||||
|
||||
Passing this verifier proves the bounded P0 operation claim. It does not close
|
||||
the DACL, reparse-point, named-pipe, or Job Object gates above.
|
||||
The controlled GitHub workflow runs only default-branch code on its protected
|
||||
self-hosted Windows environment. A separate Linux job builds the matching
|
||||
native artifact. Hosted Windows CI verifies native controls and packaged
|
||||
startup separately from real Ghidra acceptance. Broader hostile-target,
|
||||
concurrently mutable-path, extraction, and IPC-security claims require their
|
||||
own acceptance and are not implied by this P0 result.
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import eslint from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ["dist/**", "node_modules/**"] },
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
{
|
||||
files: ["**/*.ts"],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/consistent-type-imports": "error",
|
||||
"@typescript-eslint/no-confusing-void-expression": "error",
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "error",
|
||||
"@typescript-eslint/no-unnecessary-condition": "error",
|
||||
"@typescript-eslint/no-unsafe-type-assertion": "error",
|
||||
"@typescript-eslint/only-throw-error": "error",
|
||||
"@typescript-eslint/require-await": "error",
|
||||
"@typescript-eslint/switch-exhaustiveness-check": "error",
|
||||
},
|
||||
},
|
||||
);
|
||||
+4
-6
@@ -55,17 +55,15 @@ fi
|
||||
platform="$(uname -s)"
|
||||
[[ "$platform" == "Darwin" || "$platform" == "Linux" ]] || fail "REA supports macOS and selected 64-bit Linux distributions."
|
||||
command -v curl >/dev/null 2>&1 || fail "curl is required. Install curl, then rerun this installer."
|
||||
command -v node >/dev/null 2>&1 || fail "Node.js 22.19+ or 24.11+ is required; install it from https://nodejs.org and retry."
|
||||
command -v node >/dev/null 2>&1 || fail "Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from https://nodejs.org and retry."
|
||||
command -v npm >/dev/null 2>&1 || fail "npm is required; install it with Node.js and retry."
|
||||
|
||||
node_version="$(node -p 'process.versions.node' 2>/dev/null || true)"
|
||||
IFS=. read -r node_major node_minor _node_patch <<<"$node_version"
|
||||
[[ "$node_major" =~ ^[0-9]+$ && "$node_minor" =~ ^[0-9]+$ ]] || fail "the active Node.js version could not be read. Check that node works and is on PATH, then retry."
|
||||
if ! ((node_major == 22 && node_minor >= 19 || node_major >= 24)); then
|
||||
fail "Node.js $node_version is unsupported; use Node.js 22.19+ or 24.11+."
|
||||
fi
|
||||
if ((node_major == 24 && node_minor < 11)); then
|
||||
fail "Node.js $node_version is unsupported; use Node.js 24.11 or newer."
|
||||
if ! [[ "$_node_patch" =~ ^[0-9]+(\+[0-9A-Za-z.-]+)?$ ]] ||
|
||||
! ((node_major == 22 && node_minor >= 19 || node_major == 24 && node_minor >= 11 || node_major >= 26)); then
|
||||
fail "Node.js $node_version is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+."
|
||||
fi
|
||||
|
||||
if [[ -n "$version" ]]; then
|
||||
|
||||
@@ -3,14 +3,7 @@
|
||||
"entry": ["src/main.ts", "src/cli.ts", "src/contracts/errorSchemas.ts"],
|
||||
"project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"],
|
||||
"ignoreDependencies": ["electron"],
|
||||
"ignoreBinaries": ["ps"],
|
||||
"ignoreExportsUsedInFile": true,
|
||||
"ignoreIssues": {
|
||||
"src/domain/mobileApplicationInvestigation.ts": ["types"],
|
||||
"src/domain/objcSwiftMetadata.ts": ["types"],
|
||||
"src/domain/interfaceBuilderGraph.ts": ["types"],
|
||||
"src/domain/nativeInvestigationGraph.ts": ["types"]
|
||||
},
|
||||
"rules": {
|
||||
"files": "error",
|
||||
"dependencies": "error",
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# Windows native controls
|
||||
|
||||
REA ships one Windows x64 Node-API 8 addon. Users do not compile it or install
|
||||
another runtime. Ghidra and a full JDK remain bring-your-own prerequisites.
|
||||
The addon uses Windows 10+ APIs and admits fixed local NTFS volumes only.
|
||||
|
||||
The package owns the artifact path. The loader checks the package version,
|
||||
ABI, Node-API compatibility, PE architecture, and SHA-256 before loading it.
|
||||
There is no environment override for loading an arbitrary native module.
|
||||
|
||||
## Build
|
||||
|
||||
The artifact lane requires Linux, MinGW-w64 `x86_64-w64-mingw32-g++`,
|
||||
`x86_64-w64-mingw32-dlltool`, and `tar`. It never installs these commands.
|
||||
Node-API headers are reused from the build cache or downloaded from the
|
||||
pinned official Node release with its published SHA-256 checked.
|
||||
|
||||
```sh
|
||||
npm run build:windows-native
|
||||
npm run verify:windows-native:artifact
|
||||
npm pack
|
||||
```
|
||||
|
||||
`build/` is ignored by Git. The npm file inventory includes only the addon and
|
||||
its manifest. Release CI builds both, and `prepublishOnly` rejects a missing,
|
||||
stale, mismatched, or wrong-architecture artifact. A development tarball built
|
||||
without this lane reports Windows native controls as unavailable.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Opaque Node objects carry a native type tag, resource kind, and closed state.
|
||||
A pathname, serialized object, or forged handle cannot grant authority.
|
||||
- Input admission opens every component with `OPEN_REPARSE_POINT`, verifies
|
||||
the actual NTFS object, and retains handles that deny replacement and source
|
||||
writing. An admitted child keeps each ancestor nonempty; ancestor handles
|
||||
permit unrelated writes while NTFS rejects in-place reparse conversion. Volume serial, 128-bit file ID, final handle path,
|
||||
requested path, size, and digest observations are preserved separately.
|
||||
- New runtime roots and private child objects are created relative to admitted
|
||||
parent handles with `NtCreateFile`. A protected DACL and current-user owner
|
||||
are installed at creation. Readback permits exactly the current user and SYSTEM. Child
|
||||
files inherit this boundary; private files are also checked by handle.
|
||||
- Snapshots stream through a 64 KiB buffer on a Node async worker. Cancellation
|
||||
is checked between reads, cleanup waits for settlement, and the digest
|
||||
commits to the bytes written. Descriptor and snapshot handles remain held
|
||||
until runtime cleanup.
|
||||
Snapshot ownership is single-flight per runtime: an overlapping request fails
|
||||
with `ERROR_BUSY` before acquiring cancellation ownership. The TypeScript owner
|
||||
records only the accepted promise and abort listener; aborting a rejected copy
|
||||
must not cancel the active copy.
|
||||
- `PROC_THREAD_ATTRIBUTE_JOB_LIST` assigns a suspended child atomically to an
|
||||
unnamed, non-inherited Job Object. Membership and kill-on-close policy are
|
||||
checked before resume. Breakaway is disabled, and cleanup waits for all
|
||||
job members and captured output, including detached descendants.
|
||||
- Only the suspended child's default file owner is normalized. Caller token
|
||||
privileges and ownership remain unchanged. Caller CPU affinity and process
|
||||
priority are preserved in the child. Environment names are sorted and
|
||||
deduplicated using Windows ordinal comparison, preserving their native Unicode
|
||||
semantics and caller-selected values.
|
||||
- Cleanup walks locked directory objects, removes files by handle, and unlinks
|
||||
reparse entries without traversing their targets. Normal close, cancellation,
|
||||
startup failure, and timeout release provider resources. Abrupt owner death
|
||||
kills the owned job; it can leave private runtime files for later operator
|
||||
cleanup and does not claim transactional deletion after a crash.
|
||||
|
||||
Windows failures retain their constraint, requested coordinate, Win32 code,
|
||||
and system message. Unsupported filesystems and reparse paths are distinct
|
||||
from OS access denial and missing packaged controls.
|
||||
|
||||
The bearer-token descriptor remains under its immutable private lease until
|
||||
`runtime_close` on Windows; POSIX removes the descriptor after the bridge reads
|
||||
it. Job ownership guarantees process termination on owner death, not deletion
|
||||
of private files after a crash. SUBST drive aliases and changing DOS-device
|
||||
namespaces are outside the verified P0 scope and are not detected as a separate
|
||||
alias policy. Mounted-folder paths that report a reparse tag are rejected by
|
||||
component admission; broader namespace variants remain unverified. Preserve
|
||||
requested and final handle coordinates rather than treating them as one identity.
|
||||
|
||||
## Real verification
|
||||
|
||||
```powershell
|
||||
npm run verify:windows-native
|
||||
npm run verify:ghidra:windows
|
||||
```
|
||||
|
||||
The native lane checks DACL readback independently, source write/replacement
|
||||
rejection, unrelated sibling renames, reparse rejection, immutable snapshots,
|
||||
asynchronous cancellation, cleanup containment,
|
||||
descendant termination, unrelated-process preservation, normal exit, owner
|
||||
close, and forced owner exit. An optional independently built
|
||||
`tests/fixtures/windows/processBoundary.cc` executable adds breakaway and
|
||||
caller/child token observations and an in-place ancestor reparse attempt:
|
||||
|
||||
```powershell
|
||||
node scripts/verify-windows-native.mjs . C:\fixtures\process-boundary.exe
|
||||
```
|
||||
|
||||
`nonAdminRunner.cc` is a test-only launcher for an elevated development shell.
|
||||
It duplicates an existing non-elevated Explorer token belonging to the same
|
||||
user, creates private temporary window-station objects, and launches the test
|
||||
inside a kill-on-close fixture job. It does not create accounts, modify host
|
||||
policy, or change the caller's token. A real-provider claim must additionally
|
||||
verify the packaged CLI and MCP on that ordinary-user token.
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "authority.hpp"
|
||||
|
||||
namespace rea {
|
||||
|
||||
static const napi_type_tag RESOURCE_TAG = {0x45fcbf0b49e84fe3ULL, 0x9e3e5074130de843ULL};
|
||||
|
||||
void check(napi_status status) {
|
||||
if (status != napi_ok) throw Failure("Invalid native boundary value", L"", ERROR_INVALID_PARAMETER);
|
||||
}
|
||||
napi_value object(napi_env env) { napi_value value; check(napi_create_object(env, &value)); return value; }
|
||||
napi_value null(napi_env env) { napi_value value; check(napi_get_null(env, &value)); return value; }
|
||||
napi_value string(napi_env env, const std::string& text) {
|
||||
napi_value value; check(napi_create_string_utf8(env, text.data(), text.size(), &value)); return value;
|
||||
}
|
||||
napi_value string(napi_env env, const std::wstring& text) {
|
||||
static_assert(sizeof(wchar_t) == sizeof(char16_t));
|
||||
napi_value value;
|
||||
check(napi_create_string_utf16(env, reinterpret_cast<const char16_t*>(text.data()), text.size(), &value));
|
||||
return value;
|
||||
}
|
||||
napi_value number(napi_env env, double numeric) {
|
||||
napi_value value; check(napi_create_double(env, numeric, &value)); return value;
|
||||
}
|
||||
napi_value boolean(napi_env env, bool flag) {
|
||||
napi_value value; check(napi_get_boolean(env, flag, &value)); return value;
|
||||
}
|
||||
void set(napi_env env, napi_value target, const char* key, napi_value value) {
|
||||
check(napi_set_named_property(env, target, key, value));
|
||||
}
|
||||
std::wstring wide(napi_env env, napi_value value) {
|
||||
size_t size = 0;
|
||||
check(napi_get_value_string_utf16(env, value, nullptr, 0, &size));
|
||||
require(size <= 32767, "Windows value exceeds the native string limit", L"", ERROR_INVALID_PARAMETER);
|
||||
std::vector<char16_t> buffer(size + 1);
|
||||
check(napi_get_value_string_utf16(env, value, buffer.data(), buffer.size(), &size));
|
||||
std::wstring result(reinterpret_cast<const wchar_t*>(buffer.data()), size);
|
||||
require(result.find(L'\0') == std::wstring::npos, "Windows value contains NUL", L"", ERROR_INVALID_PARAMETER);
|
||||
return result;
|
||||
}
|
||||
double numeric(napi_env env, napi_value value) {
|
||||
double result; check(napi_get_value_double(env, value, &result)); return result;
|
||||
}
|
||||
std::vector<napi_value> array(napi_env env, napi_value value) {
|
||||
bool valid; check(napi_is_array(env, value, &valid));
|
||||
require(valid, "Native arguments must be an array", L"", ERROR_INVALID_PARAMETER);
|
||||
uint32_t length; check(napi_get_array_length(env, value, &length));
|
||||
std::vector<napi_value> result(length);
|
||||
for (uint32_t index = 0; index < length; ++index)
|
||||
check(napi_get_element(env, value, index, &result[index]));
|
||||
return result;
|
||||
}
|
||||
static void finalize(napi_env, void* data, void*) { delete static_cast<Resource*>(data); }
|
||||
napi_value wrap(napi_env env, std::unique_ptr<Resource> value) {
|
||||
auto result = object(env);
|
||||
check(napi_type_tag_object(env, result, &RESOURCE_TAG));
|
||||
check(napi_wrap(env, result, value.get(), finalize, nullptr, nullptr));
|
||||
value.release();
|
||||
return result;
|
||||
}
|
||||
Resource& resource(napi_env env, napi_value value, Kind kind) {
|
||||
bool tagged = false;
|
||||
check(napi_check_object_type_tag(env, value, &RESOURCE_TAG, &tagged));
|
||||
require(tagged, "Native handle does not belong to REA", L"", ERROR_INVALID_HANDLE);
|
||||
void* pointer = nullptr; check(napi_unwrap(env, value, &pointer));
|
||||
auto& result = *static_cast<Resource*>(pointer);
|
||||
require(result.kind == kind && !result.closed, "Native handle is closed or has the wrong kind", L"", ERROR_INVALID_HANDLE);
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value failureValue(napi_env env, const Failure& failure) {
|
||||
wchar_t* message = nullptr;
|
||||
FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr, failure.win32, 0, reinterpret_cast<wchar_t*>(&message), 0, nullptr);
|
||||
std::wstring detail = message == nullptr ? L"" : message;
|
||||
if (message != nullptr) LocalFree(message);
|
||||
const std::wstring constraint(failure.constraint.begin(), failure.constraint.end());
|
||||
auto text = string(env, constraint + (failure.path.empty() ? L"" : L" [" + failure.path + L"]") +
|
||||
L" (Win32 " + std::to_wstring(failure.win32) + L"): " + detail);
|
||||
napi_value error; napi_create_error(env, nullptr, text, &error);
|
||||
const char* code = failure.win32 == ERROR_FILE_NOT_FOUND || failure.win32 == ERROR_PATH_NOT_FOUND ? "ENOENT"
|
||||
: failure.win32 == ERROR_ACCESS_DENIED ? "EACCES" : "ERR_REA_WINDOWS_NATIVE";
|
||||
set(env, error, "code", string(env, std::string(code)));
|
||||
set(env, error, "constraint", string(env, failure.constraint));
|
||||
set(env, error, "requestedPath", string(env, failure.path));
|
||||
set(env, error, "win32Code", number(env, failure.win32));
|
||||
set(env, error, "win32Message", string(env, detail));
|
||||
return error;
|
||||
}
|
||||
|
||||
static napi_value call(napi_env env, napi_callback_info info) {
|
||||
try {
|
||||
std::array<napi_value, 2> args;
|
||||
size_t count = args.size();
|
||||
check(napi_get_cb_info(env, info, &count, args.data(), nullptr, nullptr));
|
||||
require(count == 2, "Native call requires operation and arguments", L"", ERROR_INVALID_PARAMETER);
|
||||
const auto operation = wide(env, args[0]);
|
||||
const auto values = array(env, args[1]);
|
||||
if (operation == L"inspect") {
|
||||
require(values.empty(), "Inspect takes no arguments", L"", ERROR_INVALID_PARAMETER);
|
||||
return inspect(env);
|
||||
}
|
||||
if (operation.rfind(L"process_", 0) == 0) return processCall(env, operation, values);
|
||||
return filesystemCall(env, operation, values);
|
||||
} catch (const Failure& failure) {
|
||||
auto error = failureValue(env, failure);
|
||||
napi_throw(env, error);
|
||||
return nullptr;
|
||||
} catch (const std::exception& failure) {
|
||||
napi_throw_error(env, "ERR_REA_WINDOWS_NATIVE", failure.what());
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
static napi_value initialize(napi_env env, napi_value exports) {
|
||||
napi_value function;
|
||||
if (napi_create_function(env, "call", NAPI_AUTO_LENGTH, call, nullptr, &function) != napi_ok) return nullptr;
|
||||
if (napi_set_named_property(env, exports, "call", function) != napi_ok) return nullptr;
|
||||
return exports;
|
||||
}
|
||||
|
||||
} // namespace rea
|
||||
|
||||
NAPI_MODULE(NODE_GYP_MODULE_NAME, rea::initialize)
|
||||
@@ -0,0 +1,120 @@
|
||||
#pragma once
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMINMAX
|
||||
#include <windows.h>
|
||||
#include <aclapi.h>
|
||||
#include <bcrypt.h>
|
||||
#include <node_api.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace rea {
|
||||
|
||||
struct Failure : std::runtime_error {
|
||||
DWORD win32;
|
||||
std::wstring path;
|
||||
std::string constraint;
|
||||
Failure(std::string constraint, std::wstring path = L"", DWORD code = GetLastError())
|
||||
: std::runtime_error(constraint), win32(code), path(std::move(path)),
|
||||
constraint(std::move(constraint)) {}
|
||||
};
|
||||
|
||||
inline void require(bool condition, const char* constraint, const std::wstring& path = L"") {
|
||||
if (!condition) {
|
||||
const auto code = GetLastError();
|
||||
throw Failure(constraint, path, code);
|
||||
}
|
||||
}
|
||||
inline void require(bool condition, const char* constraint, const std::wstring& path, DWORD code) {
|
||||
if (!condition) throw Failure(constraint, path, code);
|
||||
}
|
||||
|
||||
class Handle {
|
||||
HANDLE value_ = INVALID_HANDLE_VALUE;
|
||||
public:
|
||||
Handle() = default;
|
||||
explicit Handle(HANDLE value) : value_(value) {}
|
||||
~Handle() { reset(); }
|
||||
Handle(const Handle&) = delete;
|
||||
Handle& operator=(const Handle&) = delete;
|
||||
Handle(Handle&& other) noexcept : value_(other.release()) {}
|
||||
Handle& operator=(Handle&& other) noexcept { reset(other.release()); return *this; }
|
||||
HANDLE get() const { return value_; }
|
||||
bool valid() const { return value_ != INVALID_HANDLE_VALUE && value_ != nullptr; }
|
||||
HANDLE release() { return std::exchange(value_, INVALID_HANDLE_VALUE); }
|
||||
void reset(HANDLE next = INVALID_HANDLE_VALUE) {
|
||||
if (valid()) CloseHandle(value_);
|
||||
value_ = next;
|
||||
}
|
||||
};
|
||||
|
||||
enum class Kind { File, Runtime, Process };
|
||||
struct Resource {
|
||||
const Kind kind;
|
||||
bool closed = false;
|
||||
explicit Resource(Kind value) : kind(value) {}
|
||||
virtual ~Resource() = default;
|
||||
};
|
||||
struct File : Resource {
|
||||
std::wstring requestedPath;
|
||||
std::wstring path;
|
||||
std::vector<Handle> handles;
|
||||
explicit File(std::wstring path) : Resource(Kind::File), requestedPath(path), path(std::move(path)) {}
|
||||
HANDLE get() const { return handles.back().get(); }
|
||||
};
|
||||
struct Runtime : Resource {
|
||||
std::wstring path;
|
||||
std::vector<Handle> parents;
|
||||
Handle directory;
|
||||
std::vector<Handle> immutableFiles;
|
||||
// Main-thread admission is single-flight; only the accepted copy owns the
|
||||
// atomic cancellation flag consumed by its async worker. ERROR_BUSY rejects
|
||||
// another copy before it can reset that flag or replace pending ownership.
|
||||
bool snapshotPending = false;
|
||||
std::atomic_bool snapshotCancelled{false};
|
||||
Runtime() : Resource(Kind::Runtime) {}
|
||||
};
|
||||
struct Process : Resource {
|
||||
Handle job, process, stdoutRead, stderrRead;
|
||||
DWORD pid = 0;
|
||||
Process() : Resource(Kind::Process) {}
|
||||
};
|
||||
|
||||
void check(napi_status status);
|
||||
napi_value failureValue(napi_env env, const Failure& failure);
|
||||
napi_value object(napi_env env);
|
||||
napi_value string(napi_env env, const std::string& value);
|
||||
napi_value string(napi_env env, const std::wstring& value);
|
||||
napi_value number(napi_env env, double value);
|
||||
napi_value boolean(napi_env env, bool value);
|
||||
napi_value null(napi_env env);
|
||||
void set(napi_env env, napi_value object, const char* key, napi_value value);
|
||||
std::wstring wide(napi_env env, napi_value value);
|
||||
double numeric(napi_env env, napi_value value);
|
||||
std::vector<napi_value> array(napi_env env, napi_value value);
|
||||
napi_value wrap(napi_env env, std::unique_ptr<Resource> resource);
|
||||
Resource& resource(napi_env env, napi_value value, Kind kind);
|
||||
|
||||
std::unique_ptr<File> openFile(const std::wstring& path, DWORD access = GENERIC_READ,
|
||||
bool directory = false);
|
||||
napi_value identity(napi_env env, HANDLE handle, const std::wstring& path);
|
||||
std::unique_ptr<Runtime> createRuntime(const std::wstring& parent, const std::wstring& prefix);
|
||||
void closeRuntime(Runtime& runtime);
|
||||
napi_value filesystemCall(napi_env env, const std::wstring& operation,
|
||||
const std::vector<napi_value>& args);
|
||||
std::unique_ptr<Process> spawnProcess(const std::wstring& command, const std::wstring& commandLine,
|
||||
const std::wstring& cwd, const std::vector<std::wstring>& environment);
|
||||
napi_value processCall(napi_env env, const std::wstring& operation,
|
||||
const std::vector<napi_value>& args);
|
||||
napi_value inspect(napi_env env);
|
||||
|
||||
} // namespace rea
|
||||
@@ -0,0 +1,531 @@
|
||||
#include "authority.hpp"
|
||||
#include <sddl.h>
|
||||
#include <winternl.h>
|
||||
|
||||
namespace rea {
|
||||
|
||||
static std::wstring hex(const unsigned char* bytes, size_t count) {
|
||||
constexpr wchar_t digits[] = L"0123456789abcdef";
|
||||
std::wstring result;
|
||||
for (size_t index = 0; index < count; ++index) {
|
||||
result += digits[bytes[index] >> 4]; result += digits[bytes[index] & 15];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::wstring localPath(std::wstring path) {
|
||||
if (path.rfind(L"\\\\?\\", 0) == 0) path.erase(0, 4);
|
||||
require(path.size() >= 3 && path[1] == L':' && path[2] == L'\\' &&
|
||||
((path[0] >= L'A' && path[0] <= L'Z') || (path[0] >= L'a' && path[0] <= L'z')),
|
||||
"Only absolute local NTFS drive paths are supported", path, ERROR_NOT_SUPPORTED);
|
||||
require(path.find(L'/', 0) == std::wstring::npos && path.find(L':', 2) == std::wstring::npos,
|
||||
"Alternate streams and non-local path syntax are unsupported", path, ERROR_NOT_SUPPORTED);
|
||||
require(GetDriveTypeW(path.substr(0, 3).c_str()) == DRIVE_FIXED,
|
||||
"Only fixed local NTFS volumes are supported", path, ERROR_NOT_SUPPORTED);
|
||||
if (path.size() > 3 && path.back() == L'\\') path.pop_back();
|
||||
require(path.size() == 3 || path.back() != L'\\', "Repeated path separators are unsupported", path, ERROR_NOT_SUPPORTED);
|
||||
size_t begin = 3;
|
||||
while (begin < path.size()) {
|
||||
const auto end = path.find(L'\\', begin);
|
||||
const auto component = path.substr(begin, end == std::wstring::npos ? end : end - begin);
|
||||
require(!component.empty() && component != L"." && component != L".." &&
|
||||
component.back() != L'.' && component.back() != L' ',
|
||||
"Ambiguous Windows path component is unsupported", path, ERROR_NOT_SUPPORTED);
|
||||
if (end == std::wstring::npos) break;
|
||||
begin = end + 1;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
static Handle openComponent(const std::wstring& path, DWORD access, bool directory, DWORD sharing = FILE_SHARE_READ) {
|
||||
// Attribute-only opens do not participate in Windows share-access checks.
|
||||
// Directory guards need FILE_LIST_DIRECTORY to prevent replacement and
|
||||
// in-place reparse conversion while descendant paths are being consumed.
|
||||
Handle handle(CreateFileW((L"\\\\?\\" + path).c_str(), access | FILE_READ_ATTRIBUTES | READ_CONTROL |
|
||||
(directory ? FILE_LIST_DIRECTORY : 0),
|
||||
sharing, nullptr, OPEN_EXISTING,
|
||||
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr));
|
||||
require(handle.valid(), "Open Windows path component failed", path);
|
||||
FILE_ATTRIBUTE_TAG_INFO info{};
|
||||
require(GetFileInformationByHandleEx(handle.get(), FileAttributeTagInfo, &info, sizeof(info)),
|
||||
"Read Windows path attributes failed", path);
|
||||
if ((info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0)
|
||||
throw Failure("Reparse path component is unsupported (tag " + std::to_string(info.ReparseTag) + ")",
|
||||
path, ERROR_NOT_SUPPORTED);
|
||||
require(((info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) == directory,
|
||||
directory ? "Expected a directory path component" : "Target is not a regular file", path,
|
||||
directory ? ERROR_PATH_NOT_FOUND : ERROR_DIRECTORY);
|
||||
require(GetFileType(handle.get()) == FILE_TYPE_DISK, "Windows object is not a disk file", path, ERROR_NOT_SUPPORTED);
|
||||
return handle;
|
||||
}
|
||||
|
||||
std::unique_ptr<File> openFile(const std::wstring& requested, DWORD access, bool directory) {
|
||||
auto result = std::make_unique<File>(localPath(requested));
|
||||
result->requestedPath = requested;
|
||||
const auto& path = result->path;
|
||||
require(directory || path.size() > 3, "Target is not a regular file", requested, ERROR_DIRECTORY);
|
||||
result->handles.push_back(openComponent(path.substr(0, 3), 0, true));
|
||||
wchar_t filesystem[64]{};
|
||||
DWORD flags = 0;
|
||||
require(GetVolumeInformationByHandleW(result->handles.front().get(), nullptr, 0, nullptr, nullptr,
|
||||
&flags, filesystem, 64), "Read volume semantics failed", path);
|
||||
require(std::wstring(filesystem) == L"NTFS" && (flags & FILE_PERSISTENT_ACLS) != 0,
|
||||
"Backing volume must be NTFS with persistent ACLs", path, ERROR_NOT_SUPPORTED);
|
||||
size_t offset = 3;
|
||||
while (offset < path.size()) {
|
||||
auto end = path.find(L'\\', offset);
|
||||
const bool final = end == std::wstring::npos;
|
||||
auto child = openComponent(path.substr(0, end), final ? access : 0, !final || directory);
|
||||
// Once the next component is pinned against deletion, its parent cannot
|
||||
// become empty and NTFS cannot convert that parent into a reparse point.
|
||||
// Relax only ancestor write sharing so unrelated child renames can proceed.
|
||||
// Independent evidence: tests/fixtures/windows/processBoundary.cc attempts
|
||||
// FSCTL_SET_REPARSE_POINT on the nonempty pinned ancestor and observes denial.
|
||||
// SUBST aliases and mounted-folder/reparse namespace variants are outside
|
||||
// the admitted P0 evidence; requested and final handle paths remain distinct.
|
||||
const auto parentPath = offset == 3 ? path.substr(0, 3) : path.substr(0, offset - 1);
|
||||
result->handles.back() = openComponent(parentPath, 0, true, FILE_SHARE_READ | FILE_SHARE_WRITE);
|
||||
result->handles.push_back(std::move(child));
|
||||
if (final) break;
|
||||
offset = end + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value identity(napi_env env, HANDLE handle, const std::wstring& path) {
|
||||
FILE_ID_INFO info{};
|
||||
require(GetFileInformationByHandleEx(handle, FileIdInfo, &info, sizeof(info)),
|
||||
"Stable 128-bit Windows file identity is unavailable", path);
|
||||
std::array<wchar_t, 32768> finalPath;
|
||||
const auto length = GetFinalPathNameByHandleW(handle, finalPath.data(), finalPath.size(), FILE_NAME_NORMALIZED);
|
||||
require(length > 0 && length < finalPath.size(), "Read final handle path failed", path);
|
||||
LARGE_INTEGER size{};
|
||||
FILE_STANDARD_INFO standard{};
|
||||
require(GetFileInformationByHandleEx(handle, FileStandardInfo, &standard, sizeof(standard)),
|
||||
"Read handle metadata failed", path);
|
||||
size = standard.EndOfFile;
|
||||
require(size.QuadPart >= 0 && size.QuadPart <= 9007199254740991LL, "File size exceeds exact numeric representation", path,
|
||||
ERROR_NOT_SUPPORTED);
|
||||
auto result = object(env);
|
||||
set(env, result, "requestedPath", string(env, path));
|
||||
set(env, result, "finalPath", string(env, std::wstring(finalPath.data(), length)));
|
||||
set(env, result, "filesystem", string(env, std::string("NTFS")));
|
||||
std::array<unsigned char, 8> serial;
|
||||
for (size_t index = 0; index < serial.size(); ++index)
|
||||
serial[index] = static_cast<unsigned char>(info.VolumeSerialNumber >> ((7 - index) * 8));
|
||||
set(env, result, "volumeSerial", string(env, hex(serial.data(), serial.size())));
|
||||
set(env, result, "fileId", string(env, hex(info.FileId.Identifier, 16)));
|
||||
set(env, result, "size", number(env, static_cast<double>(size.QuadPart)));
|
||||
set(env, result, "directory", boolean(env, standard.Directory != FALSE));
|
||||
return result;
|
||||
}
|
||||
|
||||
struct Security {
|
||||
std::vector<unsigned char> user;
|
||||
std::array<unsigned char, SECURITY_MAX_SID_SIZE> system{};
|
||||
std::vector<unsigned char> acl;
|
||||
SECURITY_DESCRIPTOR descriptor{};
|
||||
PSID userSid() const { return reinterpret_cast<const TOKEN_USER*>(user.data())->User.Sid; }
|
||||
Security() {
|
||||
Handle token;
|
||||
HANDLE raw;
|
||||
require(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &raw), "Read current user token failed");
|
||||
token.reset(raw);
|
||||
DWORD size = 0;
|
||||
GetTokenInformation(token.get(), TokenUser, nullptr, 0, &size);
|
||||
user.resize(size);
|
||||
require(GetTokenInformation(token.get(), TokenUser, user.data(), size, &size), "Read current user SID failed");
|
||||
DWORD systemSize = system.size();
|
||||
require(CreateWellKnownSid(WinLocalSystemSid, nullptr, system.data(), &systemSize), "Create SYSTEM SID failed");
|
||||
acl.resize(sizeof(ACL) + 2 * (sizeof(ACCESS_ALLOWED_ACE) - sizeof(DWORD)) + GetLengthSid(userSid()) + systemSize);
|
||||
auto access = reinterpret_cast<PACL>(acl.data());
|
||||
require(InitializeAcl(access, acl.size(), ACL_REVISION), "Initialize private DACL failed");
|
||||
for (PSID sid : {userSid(), static_cast<PSID>(system.data())})
|
||||
require(AddAccessAllowedAceEx(access, ACL_REVISION, OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE,
|
||||
FILE_ALL_ACCESS, sid), "Populate private DACL failed");
|
||||
require(InitializeSecurityDescriptor(&descriptor, SECURITY_DESCRIPTOR_REVISION) &&
|
||||
SetSecurityDescriptorOwner(&descriptor, userSid(), FALSE) &&
|
||||
SetSecurityDescriptorDacl(&descriptor, TRUE, access, FALSE) &&
|
||||
SetSecurityDescriptorControl(&descriptor, SE_DACL_PROTECTED, SE_DACL_PROTECTED),
|
||||
"Create protected security descriptor failed");
|
||||
}
|
||||
};
|
||||
|
||||
static Handle createRelative(HANDLE parent, const std::wstring& name, DWORD access, bool directory,
|
||||
Security& security, const std::wstring& path) {
|
||||
require(!name.empty() && name.find_first_of(L"\\/:\"<>|?*") == std::wstring::npos && name.size() < 32768,
|
||||
"Private object requires one filename component", path, ERROR_INVALID_PARAMETER);
|
||||
UNICODE_STRING coordinate{};
|
||||
coordinate.Buffer = const_cast<wchar_t*>(name.data());
|
||||
coordinate.Length = static_cast<USHORT>(name.size() * sizeof(wchar_t));
|
||||
coordinate.MaximumLength = coordinate.Length;
|
||||
OBJECT_ATTRIBUTES attributes;
|
||||
InitializeObjectAttributes(&attributes, &coordinate, OBJ_CASE_INSENSITIVE, parent, &security.descriptor);
|
||||
IO_STATUS_BLOCK observation{};
|
||||
HANDLE raw = INVALID_HANDLE_VALUE;
|
||||
const auto status = NtCreateFile(&raw, access | SYNCHRONIZE, &attributes, &observation, nullptr,
|
||||
FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_CREATE,
|
||||
FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_REPARSE_POINT |
|
||||
(directory ? FILE_DIRECTORY_FILE : FILE_NON_DIRECTORY_FILE), nullptr, 0);
|
||||
if (status < 0) throw Failure("Create private object relative to its admitted parent failed", path,
|
||||
RtlNtStatusToDosError(status));
|
||||
Handle result(raw);
|
||||
require(result.valid() && observation.Information == FILE_CREATED,
|
||||
"Private creation did not return a new object handle", path, ERROR_ACCESS_DENIED);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void verifyPrivate(HANDLE handle, const std::wstring& path, bool root) {
|
||||
Security expected;
|
||||
PSID owner = nullptr;
|
||||
PACL acl = nullptr;
|
||||
PSECURITY_DESCRIPTOR raw = nullptr;
|
||||
const DWORD code = GetSecurityInfo(handle, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
|
||||
&owner, nullptr, &acl, nullptr, &raw);
|
||||
if (code != ERROR_SUCCESS) throw Failure("Read private DACL by handle failed", path, code);
|
||||
const auto descriptor = std::unique_ptr<void, decltype(&LocalFree)>(raw, LocalFree);
|
||||
require(owner != nullptr && EqualSid(owner, expected.userSid()), "Private object owner is not the current user", path,
|
||||
ERROR_ACCESS_DENIED);
|
||||
SECURITY_DESCRIPTOR_CONTROL control;
|
||||
DWORD revision;
|
||||
require(GetSecurityDescriptorControl(raw, &control, &revision), "Read DACL control flags failed", path);
|
||||
require(acl != nullptr && (control & SE_DACL_PRESENT) != 0 && (!root || (control & SE_DACL_PROTECTED) != 0),
|
||||
"Private object requires a present protected DACL", path, ERROR_ACCESS_DENIED);
|
||||
require(acl->AceCount == 2, "Private DACL contains unexpected trustees", path, ERROR_ACCESS_DENIED);
|
||||
bool user = false, system = false;
|
||||
for (DWORD index = 0; index < acl->AceCount; ++index) {
|
||||
void* rawAce;
|
||||
require(GetAce(acl, index, &rawAce), "Read private DACL entry failed", path);
|
||||
const auto ace = static_cast<ACCESS_ALLOWED_ACE*>(rawAce);
|
||||
require(ace->Header.AceType == ACCESS_ALLOWED_ACE_TYPE && ace->Mask == FILE_ALL_ACCESS &&
|
||||
(ace->Header.AceFlags & INHERIT_ONLY_ACE) == 0 &&
|
||||
(!root || (ace->Header.AceFlags & (OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE)) ==
|
||||
(OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE)),
|
||||
"Private DACL contains unsupported rights or ACE type", path, ERROR_ACCESS_DENIED);
|
||||
PSID sid = const_cast<DWORD*>(&ace->SidStart);
|
||||
if (EqualSid(sid, expected.userSid())) user = true;
|
||||
else if (EqualSid(sid, expected.system.data())) system = true;
|
||||
else throw Failure("Private DACL permits an unrelated principal", path, ERROR_ACCESS_DENIED);
|
||||
}
|
||||
require(user && system, "Private DACL must allow only current user and SYSTEM", path, ERROR_ACCESS_DENIED);
|
||||
}
|
||||
|
||||
std::unique_ptr<Runtime> createRuntime(const std::wstring& parentPath, const std::wstring& prefix) {
|
||||
auto parent = openFile(parentPath, 0, true);
|
||||
require(!prefix.empty() && prefix.find_first_of(L"\\/:\"<>|?*") == std::wstring::npos,
|
||||
"Runtime prefix must be one filename component", parentPath, ERROR_INVALID_PARAMETER);
|
||||
std::array<unsigned char, 16> random;
|
||||
require(BCryptGenRandom(nullptr, random.data(), random.size(), BCRYPT_USE_SYSTEM_PREFERRED_RNG) == 0,
|
||||
"Generate runtime identity failed", parentPath, ERROR_GEN_FAILURE);
|
||||
auto result = std::make_unique<Runtime>();
|
||||
result->path = parent->path + L"\\" + prefix + hex(random.data(), random.size());
|
||||
Security security;
|
||||
result->directory = createRelative(parent->get(), prefix + hex(random.data(), random.size()),
|
||||
DELETE | FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES | READ_CONTROL,
|
||||
true, security, result->path);
|
||||
try {
|
||||
verifyPrivate(result->directory.get(), result->path, true);
|
||||
// The owned child pins the parent nonempty before write sharing is relaxed.
|
||||
parent->handles.back() = openComponent(parent->path, 0, true, FILE_SHARE_READ | FILE_SHARE_WRITE);
|
||||
result->parents = std::move(parent->handles);
|
||||
} catch (...) {
|
||||
FILE_DISPOSITION_INFO deletion{TRUE};
|
||||
SetFileInformationByHandle(result->directory.get(), FileDispositionInfo, &deletion, sizeof(deletion));
|
||||
throw;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::wstring relativePath(Runtime& root, const std::wstring& relative) {
|
||||
require(!relative.empty() && relative[0] != L'\\' && relative.find(L':') == std::wstring::npos,
|
||||
"Runtime coordinate must be a relative path", relative, ERROR_INVALID_PARAMETER);
|
||||
return localPath(root.path + L"\\" + relative);
|
||||
}
|
||||
|
||||
static std::unique_ptr<File> runtimeDirectory(Runtime& root, const std::wstring& path) {
|
||||
auto result = std::make_unique<File>(path);
|
||||
HANDLE duplicate;
|
||||
require(DuplicateHandle(GetCurrentProcess(), root.directory.get(), GetCurrentProcess(), &duplicate, 0, FALSE,
|
||||
DUPLICATE_SAME_ACCESS), "Duplicate private directory handle failed", path);
|
||||
result->handles.emplace_back(duplicate);
|
||||
size_t offset = root.path.size() + 1;
|
||||
while (offset < path.size()) {
|
||||
const auto end = path.find(L'\\', offset);
|
||||
result->handles.push_back(openComponent(path.substr(0, end), 0, true));
|
||||
if (end == std::wstring::npos) break;
|
||||
offset = end + 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void directory(Runtime& root, const std::wstring& relative) {
|
||||
const auto path = relativePath(root, relative);
|
||||
const auto parentPath = path.substr(0, path.find_last_of(L'\\'));
|
||||
auto parent = runtimeDirectory(root, parentPath);
|
||||
verifyPrivate(parent->get(), parentPath, parentPath == root.path);
|
||||
Security security;
|
||||
try {
|
||||
auto created = createRelative(parent->get(), path.substr(path.find_last_of(L'\\') + 1),
|
||||
FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES | READ_CONTROL,
|
||||
true, security, path);
|
||||
verifyPrivate(created.get(), path, false);
|
||||
} catch (const Failure& failure) {
|
||||
if (failure.win32 != ERROR_ALREADY_EXISTS && failure.win32 != ERROR_FILE_EXISTS) throw;
|
||||
auto existing = runtimeDirectory(root, path);
|
||||
verifyPrivate(existing->get(), path, false);
|
||||
}
|
||||
}
|
||||
|
||||
static Handle createPrivateFile(Runtime& root, const std::wstring& relative) {
|
||||
const auto path = relativePath(root, relative);
|
||||
const auto parentPath = path.substr(0, path.find_last_of(L'\\'));
|
||||
auto parent = runtimeDirectory(root, parentPath);
|
||||
verifyPrivate(parent->get(), parentPath, parentPath == root.path);
|
||||
Security security;
|
||||
auto file = createRelative(parent->get(), path.substr(path.find_last_of(L'\\') + 1),
|
||||
GENERIC_WRITE | GENERIC_READ | READ_CONTROL, false, security, path);
|
||||
verifyPrivate(file.get(), path, false);
|
||||
return file;
|
||||
}
|
||||
|
||||
static void writeBytes(HANDLE file, const void* bytes, DWORD count, const std::wstring& path) {
|
||||
DWORD written;
|
||||
require(WriteFile(file, bytes, count, &written, nullptr) && written == count, "Write private file failed", path);
|
||||
}
|
||||
|
||||
static void dispose(HANDLE handle, const std::wstring& path) {
|
||||
FILE_BASIC_INFO info{};
|
||||
require(GetFileInformationByHandleEx(handle, FileBasicInfo, &info, sizeof(info)), "Read cleanup attributes failed", path);
|
||||
if ((info.FileAttributes & FILE_ATTRIBUTE_READONLY) != 0) {
|
||||
info.FileAttributes &= ~FILE_ATTRIBUTE_READONLY;
|
||||
require(SetFileInformationByHandle(handle, FileBasicInfo, &info, sizeof(info)), "Clear owned readonly attribute failed", path);
|
||||
}
|
||||
FILE_DISPOSITION_INFO deletion{TRUE};
|
||||
require(SetFileInformationByHandle(handle, FileDispositionInfo, &deletion, sizeof(deletion)), "Delete owned object by handle failed", path);
|
||||
}
|
||||
|
||||
void closeRuntime(Runtime& root) {
|
||||
if (root.closed) return;
|
||||
// Windows keeps the bearer descriptor and snapshot leases until this owner
|
||||
// cleanup. POSIX removes its consumed descriptor earlier. Abrupt owner death
|
||||
// closes process jobs but cannot run this walk, so private files can remain.
|
||||
root.immutableFiles.clear();
|
||||
struct ReleaseRuntime {
|
||||
Runtime& root;
|
||||
~ReleaseRuntime() { root.directory.reset(); root.parents.clear(); root.closed = true; }
|
||||
} release{root};
|
||||
// Each traversed directory stays locked against replacement. Reparse entries
|
||||
// are unlinked as objects; their targets are never enumerated or deleted.
|
||||
struct Frame {
|
||||
Handle directory;
|
||||
HANDLE search = INVALID_HANDLE_VALUE;
|
||||
std::wstring path;
|
||||
WIN32_FIND_DATAW next{};
|
||||
bool pending = false;
|
||||
Frame(Handle handle, std::wstring path) : directory(std::move(handle)), path(std::move(path)) {
|
||||
search = FindFirstFileExW((L"\\\\?\\" + this->path + L"\\*").c_str(), FindExInfoBasic, &next,
|
||||
FindExSearchNameMatch, nullptr, 0);
|
||||
pending = search != INVALID_HANDLE_VALUE;
|
||||
require(pending || GetLastError() == ERROR_FILE_NOT_FOUND, "Enumerate runtime cleanup failed", this->path);
|
||||
}
|
||||
~Frame() { if (search != INVALID_HANDLE_VALUE) FindClose(search); }
|
||||
};
|
||||
std::vector<std::unique_ptr<Frame>> frames;
|
||||
HANDLE duplicate;
|
||||
require(DuplicateHandle(GetCurrentProcess(), root.directory.get(), GetCurrentProcess(), &duplicate, 0, FALSE,
|
||||
DUPLICATE_SAME_ACCESS), "Duplicate runtime cleanup handle failed", root.path);
|
||||
frames.push_back(std::make_unique<Frame>(Handle(duplicate), root.path));
|
||||
while (!frames.empty()) {
|
||||
auto& frame = *frames.back();
|
||||
if (!frame.pending) {
|
||||
if (frame.search != INVALID_HANDLE_VALUE) { FindClose(frame.search); frame.search = INVALID_HANDLE_VALUE; }
|
||||
dispose(frame.directory.get(), frame.path);
|
||||
frames.pop_back();
|
||||
continue;
|
||||
}
|
||||
const auto name = std::wstring(frame.next.cFileName);
|
||||
frame.pending = FindNextFileW(frame.search, &frame.next) != FALSE;
|
||||
require(frame.pending || GetLastError() == ERROR_NO_MORE_FILES, "Continue runtime cleanup enumeration failed", frame.path);
|
||||
if (name == L"." || name == L"..") continue;
|
||||
const auto path = frame.path + L"\\" + name;
|
||||
Handle child(CreateFileW((L"\\\\?\\" + path).c_str(), DELETE | GENERIC_READ | FILE_WRITE_ATTRIBUTES,
|
||||
FILE_SHARE_READ, nullptr, OPEN_EXISTING,
|
||||
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr));
|
||||
require(child.valid(), "Open owned cleanup object failed", path);
|
||||
FILE_ATTRIBUTE_TAG_INFO info{};
|
||||
require(GetFileInformationByHandleEx(child.get(), FileAttributeTagInfo, &info, sizeof(info)), "Read cleanup object tag failed", path);
|
||||
if ((info.FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0 && (info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0)
|
||||
frames.push_back(std::make_unique<Frame>(std::move(child), path));
|
||||
else dispose(child.get(), path);
|
||||
}
|
||||
}
|
||||
|
||||
struct SnapshotWork {
|
||||
napi_env env;
|
||||
napi_async_work work = nullptr;
|
||||
napi_deferred deferred = nullptr;
|
||||
napi_ref rootReference = nullptr;
|
||||
Runtime* root;
|
||||
std::unique_ptr<File> source;
|
||||
Handle destination;
|
||||
std::wstring path;
|
||||
std::wstring digest;
|
||||
std::unique_ptr<Failure> failure;
|
||||
SnapshotWork(napi_env env, Runtime& root) : env(env), root(&root) {}
|
||||
};
|
||||
|
||||
static void copySnapshot(napi_env, void* pointer) {
|
||||
auto& work = *static_cast<SnapshotWork*>(pointer);
|
||||
try {
|
||||
BCRYPT_ALG_HANDLE algorithm = nullptr;
|
||||
require(BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA256_ALGORITHM, nullptr, 0) == 0,
|
||||
"Initialize snapshot SHA-256 failed", work.source->path, ERROR_GEN_FAILURE);
|
||||
struct Algorithm { BCRYPT_ALG_HANDLE handle; ~Algorithm() { BCryptCloseAlgorithmProvider(handle, 0); } } owner{algorithm};
|
||||
BCRYPT_HASH_HANDLE hash = nullptr;
|
||||
require(BCryptCreateHash(algorithm, &hash, nullptr, 0, nullptr, 0, 0) == 0,
|
||||
"Create snapshot SHA-256 state failed", work.source->path, ERROR_GEN_FAILURE);
|
||||
struct Hash { BCRYPT_HASH_HANDLE handle; ~Hash() { BCryptDestroyHash(handle); } } hashOwner{hash};
|
||||
std::array<unsigned char, 65536> buffer;
|
||||
while (true) {
|
||||
require(!work.root->snapshotCancelled.load(), "Admitted snapshot was cancelled", work.path, ERROR_OPERATION_ABORTED);
|
||||
DWORD count;
|
||||
require(ReadFile(work.source->get(), buffer.data(), buffer.size(), &count, nullptr),
|
||||
"Read admitted source handle failed", work.source->path);
|
||||
if (count == 0) break;
|
||||
writeBytes(work.destination.get(), buffer.data(), count, work.path);
|
||||
require(BCryptHashData(hash, buffer.data(), count, 0) == 0,
|
||||
"Hash admitted snapshot bytes failed", work.source->path, ERROR_GEN_FAILURE);
|
||||
}
|
||||
std::array<unsigned char, 32> digest;
|
||||
require(BCryptFinishHash(hash, digest.data(), digest.size(), 0) == 0,
|
||||
"Finish snapshot SHA-256 failed", work.source->path, ERROR_GEN_FAILURE);
|
||||
require(FlushFileBuffers(work.destination.get()), "Flush private snapshot failed", work.path);
|
||||
work.digest = hex(digest.data(), digest.size());
|
||||
} catch (const Failure& failure) {
|
||||
work.failure = std::make_unique<Failure>(failure);
|
||||
} catch (const std::exception& failure) {
|
||||
work.failure = std::make_unique<Failure>(failure.what(), work.path, ERROR_GEN_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
static void completeSnapshot(napi_env env, napi_status status, void* pointer) {
|
||||
std::unique_ptr<SnapshotWork> work(static_cast<SnapshotWork*>(pointer));
|
||||
work->root->snapshotPending = false;
|
||||
try {
|
||||
if (work->failure) throw *work->failure;
|
||||
require(status == napi_ok && !work->root->snapshotCancelled.load(), "Native snapshot worker was cancelled", work->path, ERROR_OPERATION_ABORTED);
|
||||
auto result = object(env);
|
||||
set(env, result, "sha256", string(env, work->digest));
|
||||
set(env, result, "source", identity(env, work->source->get(), work->source->requestedPath));
|
||||
set(env, result, "snapshot", identity(env, work->destination.get(), work->path));
|
||||
work->root->immutableFiles.push_back(std::move(work->destination));
|
||||
check(napi_resolve_deferred(env, work->deferred, result));
|
||||
} catch (const Failure& failure) {
|
||||
napi_reject_deferred(env, work->deferred, failureValue(env, failure));
|
||||
} catch (const std::exception& failure) {
|
||||
napi_reject_deferred(env, work->deferred,
|
||||
failureValue(env, Failure(failure.what(), work->path, ERROR_GEN_FAILURE)));
|
||||
}
|
||||
napi_delete_reference(env, work->rootReference);
|
||||
napi_delete_async_work(env, work->work);
|
||||
}
|
||||
|
||||
static napi_value snapshot(napi_env env, napi_value rootValue, Runtime& root,
|
||||
const std::wstring& sourcePath, const std::wstring& relative) {
|
||||
auto work = std::make_unique<SnapshotWork>(env, root);
|
||||
work->source = openFile(sourcePath);
|
||||
work->path = relativePath(root, relative);
|
||||
work->destination = createPrivateFile(root, relative);
|
||||
napi_value promise;
|
||||
check(napi_create_promise(env, &work->deferred, &promise));
|
||||
check(napi_create_reference(env, rootValue, 1, &work->rootReference));
|
||||
const auto label = string(env, std::string("REA admitted Windows snapshot"));
|
||||
root.snapshotCancelled.store(false);
|
||||
try {
|
||||
check(napi_create_async_work(env, nullptr, label, copySnapshot, completeSnapshot, work.get(), &work->work));
|
||||
check(napi_queue_async_work(env, work->work));
|
||||
} catch (...) {
|
||||
if (work->work) napi_delete_async_work(env, work->work);
|
||||
napi_delete_reference(env, work->rootReference);
|
||||
throw;
|
||||
}
|
||||
root.snapshotPending = true;
|
||||
work.release();
|
||||
return promise;
|
||||
}
|
||||
|
||||
napi_value filesystemCall(napi_env env, const std::wstring& operation, const std::vector<napi_value>& args) {
|
||||
const auto expected = operation == L"open" || operation == L"close" || operation == L"runtime_close" || operation == L"runtime_snapshot_cancel" ? 1
|
||||
: operation == L"runtime_create" || operation == L"runtime_mkdir" || operation == L"runtime_open" ? 2 : 3;
|
||||
require(args.size() == static_cast<size_t>(expected), "Wrong native filesystem argument count", L"", ERROR_INVALID_PARAMETER);
|
||||
if (operation == L"open") {
|
||||
auto file = openFile(wide(env, args[0]));
|
||||
auto result = identity(env, file->get(), file->requestedPath);
|
||||
set(env, result, "handle", wrap(env, std::move(file)));
|
||||
return result;
|
||||
}
|
||||
if (operation == L"close") {
|
||||
auto& file = static_cast<File&>(resource(env, args[0], Kind::File));
|
||||
file.handles.clear(); file.closed = true;
|
||||
return null(env);
|
||||
}
|
||||
if (operation == L"read") {
|
||||
auto& file = static_cast<File&>(resource(env, args[0], Kind::File));
|
||||
const auto position = numeric(env, args[1]);
|
||||
const auto length = numeric(env, args[2]);
|
||||
require(position >= 0 && position <= 9007199254740991.0 && position == static_cast<int64_t>(position) &&
|
||||
length >= 0 && length <= 65536 && length == static_cast<DWORD>(length),
|
||||
"Read coordinates exceed the native chunk boundary", file.path, ERROR_INVALID_PARAMETER);
|
||||
LARGE_INTEGER offset; offset.QuadPart = static_cast<int64_t>(position);
|
||||
require(SetFilePointerEx(file.get(), offset, nullptr, FILE_BEGIN), "Seek admitted file handle failed", file.path);
|
||||
std::array<unsigned char, 65536> buffer;
|
||||
DWORD count;
|
||||
require(ReadFile(file.get(), buffer.data(), static_cast<DWORD>(length), &count, nullptr), "Read admitted file handle failed", file.path);
|
||||
napi_value result;
|
||||
check(napi_create_buffer_copy(env, count, buffer.data(), nullptr, &result));
|
||||
return result;
|
||||
}
|
||||
if (operation == L"runtime_create") {
|
||||
auto root = createRuntime(wide(env, args[0]), wide(env, args[1]));
|
||||
auto result = identity(env, root->directory.get(), root->path);
|
||||
set(env, result, "path", string(env, root->path));
|
||||
set(env, result, "privateDacl", boolean(env, true));
|
||||
set(env, result, "handle", wrap(env, std::move(root)));
|
||||
return result;
|
||||
}
|
||||
auto& root = static_cast<Runtime&>(resource(env, args[0], Kind::Runtime));
|
||||
if (operation == L"runtime_snapshot_cancel") { root.snapshotCancelled.store(true); return null(env); }
|
||||
require(!root.snapshotPending, "Runtime snapshot is still pending", root.path, ERROR_BUSY);
|
||||
// Reject overlapping operations before snapshot() resets cancellation. The
|
||||
// one accepted worker and WindowsPrivateRuntime retain the same root lease.
|
||||
if (operation == L"runtime_close") { closeRuntime(root); return null(env); }
|
||||
if (operation == L"runtime_mkdir") { directory(root, wide(env, args[1])); return null(env); }
|
||||
if (operation == L"runtime_open") {
|
||||
const auto path = relativePath(root, wide(env, args[1]));
|
||||
auto file = runtimeDirectory(root, path.substr(0, path.find_last_of(L'\\')));
|
||||
file->path = path;
|
||||
file->requestedPath = path;
|
||||
file->handles.push_back(openComponent(path, GENERIC_READ, false));
|
||||
verifyPrivate(file->get(), path, false);
|
||||
auto result = identity(env, file->get(), path);
|
||||
set(env, result, "handle", wrap(env, std::move(file)));
|
||||
return result;
|
||||
}
|
||||
if (operation == L"runtime_write") {
|
||||
bool isBuffer; check(napi_is_buffer(env, args[2], &isBuffer));
|
||||
require(isBuffer, "Private write requires a byte buffer", root.path, ERROR_INVALID_PARAMETER);
|
||||
void* bytes; size_t length; check(napi_get_buffer_info(env, args[2], &bytes, &length));
|
||||
require(length <= MAXDWORD, "Private write exceeds Win32 byte representation", root.path, ERROR_INVALID_PARAMETER);
|
||||
auto file = createPrivateFile(root, wide(env, args[1]));
|
||||
writeBytes(file.get(), bytes, static_cast<DWORD>(length), root.path);
|
||||
require(FlushFileBuffers(file.get()), "Flush private runtime file failed", root.path);
|
||||
root.immutableFiles.push_back(std::move(file));
|
||||
return null(env);
|
||||
}
|
||||
if (operation == L"runtime_snapshot") {
|
||||
return snapshot(env, args[0], root, wide(env, args[1]), wide(env, args[2]));
|
||||
}
|
||||
throw Failure("Unknown native filesystem operation", L"", ERROR_INVALID_PARAMETER);
|
||||
}
|
||||
|
||||
} // namespace rea
|
||||
@@ -0,0 +1,271 @@
|
||||
#include "authority.hpp"
|
||||
|
||||
namespace rea {
|
||||
|
||||
class Attributes {
|
||||
std::vector<unsigned char> storage_;
|
||||
public:
|
||||
explicit Attributes(DWORD count) {
|
||||
SIZE_T size = 0;
|
||||
InitializeProcThreadAttributeList(nullptr, count, 0, &size);
|
||||
storage_.resize(size);
|
||||
require(InitializeProcThreadAttributeList(get(), count, 0, &size), "Initialize process attributes failed");
|
||||
}
|
||||
~Attributes() { DeleteProcThreadAttributeList(get()); }
|
||||
LPPROC_THREAD_ATTRIBUTE_LIST get() { return reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(storage_.data()); }
|
||||
void add(DWORD_PTR key, void* value, SIZE_T size) {
|
||||
require(UpdateProcThreadAttribute(get(), 0, key, value, size, nullptr, nullptr),
|
||||
"Windows atomic process attribute is unavailable");
|
||||
}
|
||||
};
|
||||
|
||||
static Handle createJob() {
|
||||
Handle job(CreateJobObjectW(nullptr, nullptr));
|
||||
require(job.valid(), "Create owned Job Object failed");
|
||||
JOBOBJECT_EXTENDED_LIMIT_INFORMATION policy{};
|
||||
policy.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
require(SetInformationJobObject(job.get(), JobObjectExtendedLimitInformation, &policy, sizeof(policy)),
|
||||
"Apply kill-on-owner-close Job Object policy failed");
|
||||
JOBOBJECT_EXTENDED_LIMIT_INFORMATION actual{};
|
||||
require(QueryInformationJobObject(job.get(), JobObjectExtendedLimitInformation, &actual, sizeof(actual), nullptr),
|
||||
"Read Job Object policy failed");
|
||||
require(actual.BasicLimitInformation.LimitFlags == JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
|
||||
"Job Object policy permits unexpected breakaway", L"", ERROR_ACCESS_DENIED);
|
||||
return job;
|
||||
}
|
||||
|
||||
static void pipe(Handle& read, Handle& write) {
|
||||
SECURITY_ATTRIBUTES attributes{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
|
||||
HANDLE rawRead, rawWrite;
|
||||
require(CreatePipe(&rawRead, &rawWrite, &attributes, 0), "Create owned process output pipe failed");
|
||||
read.reset(rawRead); write.reset(rawWrite);
|
||||
require(SetHandleInformation(read.get(), HANDLE_FLAG_INHERIT, 0), "Protect process output handle inheritance failed");
|
||||
}
|
||||
|
||||
std::unique_ptr<Process> spawnProcess(const std::wstring& command, const std::wstring& commandLine,
|
||||
const std::wstring& cwd, const std::vector<std::wstring>& environment) {
|
||||
require(command.size() >= 3 && command[1] == L':' && command[2] == L'\\',
|
||||
"Owned Windows process requires an absolute executable path", command, ERROR_INVALID_PARAMETER);
|
||||
auto result = std::make_unique<Process>();
|
||||
result->job = createJob();
|
||||
Handle stdoutWrite, stderrWrite;
|
||||
pipe(result->stdoutRead, stdoutWrite); pipe(result->stderrRead, stderrWrite);
|
||||
SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
|
||||
Handle input(CreateFileW(L"NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
&security, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
|
||||
require(input.valid(), "Open owned process null input failed");
|
||||
Attributes attributes(2);
|
||||
HANDLE job = result->job.get();
|
||||
attributes.add(PROC_THREAD_ATTRIBUTE_JOB_LIST, &job, sizeof(job));
|
||||
std::array<HANDLE, 3> inherited{input.get(), stdoutWrite.get(), stderrWrite.get()};
|
||||
attributes.add(PROC_THREAD_ATTRIBUTE_HANDLE_LIST, inherited.data(), sizeof(inherited));
|
||||
STARTUPINFOEXW startup{};
|
||||
startup.StartupInfo.cb = sizeof(startup);
|
||||
startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
||||
startup.StartupInfo.hStdInput = input.get();
|
||||
startup.StartupInfo.hStdOutput = stdoutWrite.get();
|
||||
startup.StartupInfo.hStdError = stderrWrite.get();
|
||||
startup.lpAttributeList = attributes.get();
|
||||
// Windows environment names use ordinal case-insensitive comparison, not
|
||||
// locale collation or JavaScript's expanding Unicode uppercase conversion.
|
||||
auto orderedEnvironment = environment;
|
||||
auto keyLength = [&](const std::wstring& entry) {
|
||||
const auto end = entry.find(L'=', entry.front() == L'=' ? 1 : 0);
|
||||
require(end != std::wstring::npos && end > 0, "Invalid child environment name", command,
|
||||
ERROR_INVALID_PARAMETER);
|
||||
return static_cast<int>(end);
|
||||
};
|
||||
for (const auto& entry : orderedEnvironment) {
|
||||
require(!entry.empty(), "Empty child environment entry", command, ERROR_INVALID_PARAMETER);
|
||||
keyLength(entry);
|
||||
}
|
||||
auto compareNames = [&](const std::wstring& left, const std::wstring& right) {
|
||||
const auto result = CompareStringOrdinal(left.data(), keyLength(left), right.data(), keyLength(right), TRUE);
|
||||
require(result != 0, "Compare Windows environment names failed", command);
|
||||
return result;
|
||||
};
|
||||
std::stable_sort(orderedEnvironment.begin(), orderedEnvironment.end(),
|
||||
[&](const std::wstring& left, const std::wstring& right) {
|
||||
return compareNames(left, right) == CSTR_LESS_THAN;
|
||||
});
|
||||
std::vector<std::wstring> uniqueEnvironment;
|
||||
for (auto& entry : orderedEnvironment) {
|
||||
if (!uniqueEnvironment.empty() && compareNames(uniqueEnvironment.back(), entry) == CSTR_EQUAL)
|
||||
uniqueEnvironment.back() = std::move(entry);
|
||||
else uniqueEnvironment.push_back(std::move(entry));
|
||||
}
|
||||
std::vector<wchar_t> variables;
|
||||
for (const auto& entry : uniqueEnvironment) {
|
||||
variables.insert(variables.end(), entry.begin(), entry.end()); variables.push_back(L'\0');
|
||||
}
|
||||
variables.push_back(L'\0');
|
||||
if (uniqueEnvironment.empty()) variables.push_back(L'\0');
|
||||
std::vector<wchar_t> line(commandLine.begin(), commandLine.end()); line.push_back(L'\0');
|
||||
PROCESS_INFORMATION process{};
|
||||
require(CreateProcessW(command.c_str(), line.data(), nullptr, nullptr, TRUE,
|
||||
CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT | CREATE_SUSPENDED | CREATE_NO_WINDOW,
|
||||
variables.data(), cwd.empty() ? nullptr : cwd.c_str(), &startup.StartupInfo, &process),
|
||||
"Create process atomically inside owned Job Object failed", command);
|
||||
result->process.reset(process.hProcess);
|
||||
Handle thread(process.hThread);
|
||||
result->pid = process.dwProcessId;
|
||||
// An elevated token may default file ownership to Administrators. Normalize
|
||||
// only the suspended child's default owner so its runtime files belong to the
|
||||
// current user; the caller's token and privileges are not modified.
|
||||
HANDLE rawToken;
|
||||
require(OpenProcessToken(result->process.get(), TOKEN_QUERY | TOKEN_ADJUST_DEFAULT, &rawToken),
|
||||
"Open suspended process default-owner token failed", command);
|
||||
Handle token(rawToken);
|
||||
DWORD tokenSize = 0;
|
||||
GetTokenInformation(token.get(), TokenUser, nullptr, 0, &tokenSize);
|
||||
std::vector<unsigned char> tokenUser(tokenSize);
|
||||
require(GetTokenInformation(token.get(), TokenUser, tokenUser.data(), tokenSize, &tokenSize),
|
||||
"Read suspended process user identity failed", command);
|
||||
TOKEN_OWNER owner{reinterpret_cast<TOKEN_USER*>(tokenUser.data())->User.Sid};
|
||||
require(SetTokenInformation(token.get(), TokenOwner, &owner, sizeof(owner)),
|
||||
"Set suspended process file owner failed", command);
|
||||
DWORD_PTR affinity = 0, systemAffinity = 0;
|
||||
require(GetProcessAffinityMask(GetCurrentProcess(), &affinity, &systemAffinity),
|
||||
"Read caller process affinity failed", command);
|
||||
if (affinity != 0)
|
||||
require(SetProcessAffinityMask(result->process.get(), affinity), "Preserve caller process affinity failed", command);
|
||||
const DWORD priority = GetPriorityClass(GetCurrentProcess());
|
||||
require(priority != 0 && SetPriorityClass(result->process.get(), priority),
|
||||
"Preserve caller process priority failed", command);
|
||||
BOOL assigned = FALSE;
|
||||
if (!IsProcessInJob(result->process.get(), result->job.get(), &assigned) || !assigned) {
|
||||
TerminateJobObject(result->job.get(), 1);
|
||||
throw Failure("Suspended process has no owned Job Object membership", command, ERROR_ACCESS_DENIED);
|
||||
}
|
||||
require(ResumeThread(thread.get()) != MAXDWORD, "Resume job-owned process failed", command);
|
||||
return result;
|
||||
}
|
||||
|
||||
static napi_value readOutput(napi_env env, Handle& read, bool& ended) {
|
||||
std::array<unsigned char, 65536> bytes;
|
||||
DWORD available = 0, count = 0;
|
||||
if (!read.valid()) ended = true;
|
||||
else if (!PeekNamedPipe(read.get(), nullptr, 0, nullptr, &available, nullptr)) {
|
||||
require(GetLastError() == ERROR_BROKEN_PIPE, "Observe owned process output failed");
|
||||
read.reset(); ended = true;
|
||||
} else if (available > 0) {
|
||||
require(ReadFile(read.get(), bytes.data(), std::min<DWORD>(available, bytes.size()), &count, nullptr),
|
||||
"Read owned process output failed");
|
||||
}
|
||||
napi_value result;
|
||||
check(napi_create_buffer_copy(env, count, bytes.data(), nullptr, &result));
|
||||
return result;
|
||||
}
|
||||
|
||||
static DWORD activeProcesses(Process& process) {
|
||||
JOBOBJECT_BASIC_ACCOUNTING_INFORMATION state{};
|
||||
require(QueryInformationJobObject(process.job.get(), JobObjectBasicAccountingInformation, &state, sizeof(state), nullptr),
|
||||
"Observe owned Job Object settlement failed");
|
||||
return state.ActiveProcesses;
|
||||
}
|
||||
|
||||
static napi_value callerToken(napi_env env) {
|
||||
HANDLE raw;
|
||||
require(OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_DUPLICATE, &raw), "Read caller token failed");
|
||||
Handle token(raw);
|
||||
DWORD size;
|
||||
TOKEN_ELEVATION elevation{};
|
||||
require(GetTokenInformation(token.get(), TokenElevation, &elevation, sizeof(elevation), &size), "Read caller elevation failed");
|
||||
std::vector<unsigned char> user(4096), owner(4096), integrity(4096);
|
||||
require(GetTokenInformation(token.get(), TokenUser, user.data(), user.size(), &size) &&
|
||||
GetTokenInformation(token.get(), TokenOwner, owner.data(), owner.size(), &size) &&
|
||||
GetTokenInformation(token.get(), TokenIntegrityLevel, integrity.data(), integrity.size(), &size),
|
||||
"Read caller token observations failed");
|
||||
HANDLE rawImpersonation;
|
||||
require(DuplicateToken(token.get(), SecurityIdentification, &rawImpersonation), "Observe caller group membership failed");
|
||||
Handle impersonation(rawImpersonation);
|
||||
std::array<unsigned char, SECURITY_MAX_SID_SIZE> administrators;
|
||||
DWORD administratorSize = administrators.size();
|
||||
require(CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, administrators.data(), &administratorSize),
|
||||
"Create administrator group identity failed");
|
||||
BOOL administrator = FALSE, assigned = FALSE;
|
||||
require(CheckTokenMembership(impersonation.get(), administrators.data(), &administrator), "Read caller administrator membership failed");
|
||||
require(IsProcessInJob(GetCurrentProcess(), nullptr, &assigned), "Read caller job membership failed");
|
||||
const auto sid = reinterpret_cast<TOKEN_MANDATORY_LABEL*>(integrity.data())->Label.Sid;
|
||||
auto result = object(env);
|
||||
set(env, result, "elevated", boolean(env, elevation.TokenIsElevated != 0));
|
||||
set(env, result, "administrator", boolean(env, administrator != FALSE));
|
||||
set(env, result, "integrityLevel", number(env, *GetSidSubAuthority(sid, *GetSidSubAuthorityCount(sid) - 1)));
|
||||
set(env, result, "defaultOwnerIsUser", boolean(env, EqualSid(reinterpret_cast<TOKEN_USER*>(user.data())->User.Sid,
|
||||
reinterpret_cast<TOKEN_OWNER*>(owner.data())->Owner)));
|
||||
set(env, result, "inJob", boolean(env, assigned != FALSE));
|
||||
return result;
|
||||
}
|
||||
|
||||
napi_value processCall(napi_env env, const std::wstring& operation, const std::vector<napi_value>& args) {
|
||||
if (operation == L"process_caller_token") {
|
||||
require(args.empty(), "Caller token observation takes no arguments", L"", ERROR_INVALID_PARAMETER);
|
||||
return callerToken(env);
|
||||
}
|
||||
if (operation == L"process_spawn") {
|
||||
require(args.size() == 4, "Wrong native process launch argument count", L"", ERROR_INVALID_PARAMETER);
|
||||
std::vector<std::wstring> environment;
|
||||
for (const auto& entry : array(env, args[3])) environment.push_back(wide(env, entry));
|
||||
auto process = spawnProcess(wide(env, args[0]), wide(env, args[1]), wide(env, args[2]), environment);
|
||||
auto result = object(env);
|
||||
set(env, result, "pid", number(env, process->pid));
|
||||
set(env, result, "jobAssigned", boolean(env, true));
|
||||
set(env, result, "killOnOwnerClose", boolean(env, true));
|
||||
set(env, result, "handle", wrap(env, std::move(process)));
|
||||
return result;
|
||||
}
|
||||
require(args.size() == 1, "Wrong native process handle argument count", L"", ERROR_INVALID_PARAMETER);
|
||||
auto& process = static_cast<Process&>(resource(env, args[0], Kind::Process));
|
||||
if (operation == L"process_poll") {
|
||||
auto result = object(env);
|
||||
bool stdoutEnded = false, stderrEnded = false;
|
||||
set(env, result, "stdout", readOutput(env, process.stdoutRead, stdoutEnded));
|
||||
set(env, result, "stderr", readOutput(env, process.stderrRead, stderrEnded));
|
||||
set(env, result, "stdoutEnded", boolean(env, stdoutEnded));
|
||||
set(env, result, "stderrEnded", boolean(env, stderrEnded));
|
||||
const auto exited = WaitForSingleObject(process.process.get(), 0);
|
||||
require(exited == WAIT_OBJECT_0 || exited == WAIT_TIMEOUT, "Observe owned process exit failed");
|
||||
DWORD code = 0;
|
||||
if (exited == WAIT_OBJECT_0) require(GetExitCodeProcess(process.process.get(), &code), "Read owned process exit code failed");
|
||||
set(env, result, "exitCode", exited == WAIT_OBJECT_0 ? number(env, code) : null(env));
|
||||
set(env, result, "activeProcesses", number(env, activeProcesses(process)));
|
||||
return result;
|
||||
}
|
||||
if (operation == L"process_alive") return number(env, activeProcesses(process));
|
||||
if (operation == L"process_terminate") {
|
||||
const auto active = activeProcesses(process);
|
||||
if (active > 0) require(TerminateJobObject(process.job.get(), 1), "Terminate owned Job Object failed");
|
||||
return boolean(env, active > 0);
|
||||
}
|
||||
if (operation == L"process_close") {
|
||||
// Closing the job is the crash-safe authority; no PID enumeration is used.
|
||||
process.job.reset(); process.process.reset(); process.stdoutRead.reset(); process.stderrRead.reset();
|
||||
process.closed = true;
|
||||
return null(env);
|
||||
}
|
||||
throw Failure("Unknown native process operation", L"", ERROR_INVALID_PARAMETER);
|
||||
}
|
||||
|
||||
napi_value inspect(napi_env env) {
|
||||
wchar_t temp[32768];
|
||||
const auto length = GetTempPathW(32768, temp);
|
||||
require(length > 0 && length < 32768, "Read temporary directory coordinate failed");
|
||||
auto root = createRuntime(std::wstring(temp, length), L"rea-native-probe-");
|
||||
auto filesystem = identity(env, root->directory.get(), root->path);
|
||||
closeRuntime(*root);
|
||||
auto job = createJob();
|
||||
Attributes attributes(1);
|
||||
HANDLE raw = job.get();
|
||||
attributes.add(PROC_THREAD_ATTRIBUTE_JOB_LIST, &raw, sizeof(raw));
|
||||
auto result = object(env);
|
||||
set(env, result, "abiVersion", number(env, 1));
|
||||
set(env, result, "nodeApiVersion", number(env, 8));
|
||||
set(env, result, "architecture", string(env, std::string("x64")));
|
||||
set(env, result, "filesystem", filesystem);
|
||||
set(env, result, "privateDacl", boolean(env, true));
|
||||
set(env, result, "atomicJobAssignment", boolean(env, true));
|
||||
set(env, result, "killOnOwnerClose", boolean(env, true));
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace rea
|
||||
Generated
+1660
-1218
File diff suppressed because it is too large
Load Diff
+59
-44
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "rea-agents",
|
||||
"mcpName": "io.github.morluto/rea",
|
||||
"version": "3.2.1",
|
||||
"version": "4.0.1",
|
||||
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
@@ -35,14 +35,16 @@
|
||||
"rea-agents": "scripts/rea.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.19.0 || >=24.11.0"
|
||||
"node": "^22.19.0 || ^24.11.0 || >=26.0.0"
|
||||
},
|
||||
"packageManager": "npm@11.16.0",
|
||||
"files": [
|
||||
"dist",
|
||||
"install.sh",
|
||||
"bridge/hopper_bridge.py",
|
||||
"bridge/native/ReaNativeUI.swift",
|
||||
"bridge/ghidra/ReaGhidraBridge.java",
|
||||
"bridge/ghidra/ReaGhidraPrepareCom.java",
|
||||
"scripts/rea.mjs",
|
||||
"scripts/electron-active-hook.cjs",
|
||||
"scripts/electron-active-hook-boundaries.cjs",
|
||||
@@ -51,7 +53,10 @@
|
||||
"scripts/prepare.mjs",
|
||||
"skills",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
"LICENSE",
|
||||
"native/windows/build/manifest.json",
|
||||
"native/windows/build/rea-windows-x64.node",
|
||||
"scripts/verify-windows-native-artifact.mjs"
|
||||
],
|
||||
"scripts": {
|
||||
"deps:check": "node scripts/check-dependency-install.mjs",
|
||||
@@ -60,44 +65,42 @@
|
||||
"mcp-registry:check": "node scripts/verify-mcp-registry-metadata.mjs",
|
||||
"mcp-catalog:generate": "npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs",
|
||||
"mcp-catalog:check": "node scripts/generate-mcp-tool-catalog.mjs --check",
|
||||
"prebuild": "npm run deps:check && npm run metadata:check && node scripts/clean-build-output.mjs",
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"prebuild": "npm run deps:check && npm run metadata:check",
|
||||
"build": "node scripts/run-exclusive.mjs build npm run build:unlocked",
|
||||
"build:unlocked": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json",
|
||||
"precheck": "npm run deps:check",
|
||||
"check": "turbo run typecheck lint format:check knip metadata:check",
|
||||
"precheck:fast": "npm run deps:check",
|
||||
"check:fast": "turbo run typecheck lint",
|
||||
"check:fast": "turbo run typecheck lint --output-logs=errors-only",
|
||||
"precheck:changed": "npm run deps:check",
|
||||
"check:changed": "turbo run typecheck lint && npm run test:changed",
|
||||
"check:changed": "npm run check:fast && npm run test:changed",
|
||||
"precheck:test": "npm run deps:check",
|
||||
"check:test": "turbo run typecheck lint format:check knip metadata:check test:run",
|
||||
"precheck:pr": "npm run deps:check",
|
||||
"check:pr": "turbo run typecheck lint format:check knip metadata:check test:run && npm run docs:check",
|
||||
"check:pr": "turbo run typecheck lint format:check knip metadata:check test:run && npm run docs:check && npm run jscpd && npm run scan:todos",
|
||||
"precheck:ci": "npm run deps:check",
|
||||
"check:ci": "turbo run typecheck lint format:check knip metadata:check jscpd scan:todos",
|
||||
"build:cached": "turbo run build",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"format": "oxfmt .",
|
||||
"format:check": "oxfmt --check .",
|
||||
"lint": "oxlint",
|
||||
"lint:fix": "oxlint --fix",
|
||||
"knip": "knip",
|
||||
"lint:dead": "knip",
|
||||
"jscpd": "jscpd src/ tests/",
|
||||
"scan:todos": "node scripts/scan-todos.mjs",
|
||||
"start": "node scripts/rea.mjs mcp",
|
||||
"test": "node scripts/run-exclusive.mjs test npm run test:with-build",
|
||||
"test": "npm run test:with-build",
|
||||
"test:with-build": "npm run build:cached && vitest run",
|
||||
"test:run": "node scripts/run-exclusive.mjs test vitest run",
|
||||
"test:local": "node scripts/run-exclusive.mjs test vitest run --changed --passWithNoTests --maxWorkers=1",
|
||||
"pretest:fast": "npm run build:cached",
|
||||
"test:fast": "vitest run --project domain --project services --project adapters --project composition --project boundary --project process-boundary --project mcp-boundary --project conformance",
|
||||
"test:run": "vitest run",
|
||||
"test:local": "node scripts/run-development-tests.mjs local",
|
||||
"test:fast": "vitest run --project domain --project services --project adapters --project composition --project conformance --project evaluation",
|
||||
"pretest:boundary": "npm run build:cached",
|
||||
"test:boundary": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global",
|
||||
"pretest:mcp": "npm run build:cached",
|
||||
"test:mcp": "vitest run --project mcp-boundary",
|
||||
"pretest:acceptance": "npm run build:cached",
|
||||
"test:acceptance": "vitest run --project acceptance",
|
||||
"pretest:changed": "npm run build:cached",
|
||||
"test:changed": "vitest run --project domain --project services --project adapters --project composition --project boundary --project process-boundary --project mcp-boundary --project conformance --project evaluation --changed",
|
||||
"test:changed": "node scripts/run-development-tests.mjs changed",
|
||||
"pretest:integration": "npm run build:cached",
|
||||
"test:integration": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global --project acceptance",
|
||||
"pretest:coverage": "npm run build:cached",
|
||||
@@ -109,14 +112,19 @@
|
||||
"test:ci:merge": "vitest --merge-reports --coverage.enabled --coverage.provider=v8",
|
||||
"pretest:verbose": "npm run build:cached",
|
||||
"test:verbose": "vitest run --reporter=verbose",
|
||||
"pretest:watch": "npm run build:cached",
|
||||
"test:watch": "vitest --project domain --project services --project adapters --project composition --project boundary --project process-boundary --project mcp-boundary --project conformance --project evaluation --changed",
|
||||
"test:watch": "vitest --project domain --project services --project adapters --project composition --project conformance --project evaluation --changed",
|
||||
"pretest:watch:all": "npm run build:cached",
|
||||
"test:watch:all": "vitest --changed",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"verify:test-temp-hygiene": "npm run build:cached && node scripts/verify-test-temp-hygiene.mjs",
|
||||
"verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs",
|
||||
"verify:apple-dispatch": "npm run build:cached && node scripts/verify-apple-dispatch.mjs",
|
||||
"verify:native-ui": "npm run build:cached && node scripts/verify-native-ui.mjs",
|
||||
"verify:keyed-archive": "npm run build:cached && node scripts/verify-keyed-archive.mjs",
|
||||
"verify:ghidra": "npm run build:cached && node scripts/verify-real-ghidra.mjs",
|
||||
"verify:ghidra:switch": "npm run build:cached && node scripts/verify-real-ghidra-switch.mjs",
|
||||
"verify:ghidra:dos": "npm run build:cached && node scripts/verify-real-ghidra-dos.mjs",
|
||||
"verify:ghidra:com": "npm run build:cached && node scripts/verify-real-ghidra-com.mjs",
|
||||
"verify:ghidra:aarch64-jump-table": "npm run build:cached && node scripts/verify-real-ghidra.mjs --aarch64-jump-table",
|
||||
"verify:interface-builder": "npm run build:cached && node scripts/verify-interface-builder.mjs",
|
||||
"verify:ghidra:cross-format": "npm run build:cached && node scripts/verify-real-ghidra.mjs --cross-format",
|
||||
@@ -125,9 +133,9 @@
|
||||
"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:replay": "npm run build:cached && node scripts/verify-real-javascript-replay.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",
|
||||
@@ -142,22 +150,28 @@
|
||||
"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:api": "typedoc",
|
||||
"docs:api:cached": "turbo run docs:api",
|
||||
"docs:generate": "node scripts/run-exclusive.mjs docs npm run docs:generate:unlocked",
|
||||
"docs:generate:unlocked": "npm run metadata:generate && npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && node scripts/generate-completion-ledger.mjs && npm run docs:api:cached && node scripts/generate-error-schema.mjs",
|
||||
"docs: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",
|
||||
"docs:check:built": "node scripts/generate-package-metadata.mjs --check && node scripts/generate-mcp-tool-catalog.mjs --check && node scripts/generate-skill-metadata.mjs --check && node scripts/generate-product-catalog.mjs --check && node scripts/generate-completion-ledger.mjs --check && node scripts/check-doc-facts.mjs && node scripts/generate-error-schema.mjs --check",
|
||||
"docs:error-schema": "npm run build:cached && node scripts/generate-error-schema.mjs",
|
||||
"prepack": "node scripts/prepack.mjs",
|
||||
"prepare": "node scripts/prepare.mjs"
|
||||
"prepare": "node scripts/prepare.mjs",
|
||||
"verify:asset-catalog": "npm run build:cached && node scripts/verify-asset-catalog.mjs",
|
||||
"verify:native-ui:permissions": "npm run build:cached && node scripts/verify-native-ui.mjs --allow-permission-denial",
|
||||
"test:focused": "node scripts/run-development-tests.mjs focused",
|
||||
"build:windows-native": "node scripts/build-windows-native.mjs",
|
||||
"verify:windows-native:artifact": "node scripts/verify-windows-native-artifact.mjs",
|
||||
"prepublishOnly": "npm run verify:windows-native:artifact",
|
||||
"verify:windows-native": "node scripts/verify-windows-native.mjs",
|
||||
"verify:ghidra:windows:package": "node scripts/verify-packaged-ghidra-windows.mjs"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
|
||||
"prettier --write",
|
||||
"oxfmt",
|
||||
"oxlint --fix"
|
||||
],
|
||||
"*.{json,md,yml,yaml}": "prettier --write"
|
||||
"*.{json,md,yml,yaml}": "oxfmt"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/parser": "8.0.4",
|
||||
@@ -166,44 +180,45 @@
|
||||
"@electron/asar": "4.2.0",
|
||||
"@jridgewell/trace-mapping": "0.3.31",
|
||||
"@lydell/node-pty": "1.1.0",
|
||||
"@modelcontextprotocol/client": "2.0.0",
|
||||
"@modelcontextprotocol/server": "2.0.0",
|
||||
"@modelcontextprotocol/client": "2.3.1",
|
||||
"@modelcontextprotocol/server": "2.3.1",
|
||||
"@xterm/addon-serialize": "0.14.0",
|
||||
"@xterm/headless": "6.0.0",
|
||||
"@zip.js/zip.js": "2.8.26",
|
||||
"canonicalize": "3.0.0",
|
||||
"cross-spawn": "7.0.6",
|
||||
"diff": "9.0.0",
|
||||
"graphology": "0.26.0",
|
||||
"graphology-shortest-path": "2.1.0",
|
||||
"ignore": "7.0.6",
|
||||
"incur": "0.4.13",
|
||||
"incur": "0.6.1",
|
||||
"isomorphic-git": "1.38.7",
|
||||
"pino": "9.14.0",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"pino": "10.4.0",
|
||||
"playwright-core": "1.58.2",
|
||||
"plist": "5.0.0",
|
||||
"semver": "7.8.5",
|
||||
"smol-toml": "^1.7.0",
|
||||
"smol-toml": "1.7.0",
|
||||
"write-file-atomic": "7.0.0",
|
||||
"ws": "8.21.0",
|
||||
"zod": "4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fast-check/vitest": "0.4.1",
|
||||
"@types/node": "24.10.1",
|
||||
"@fast-check/vitest": "0.5.0",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@types/node": "26.6.4",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/write-file-atomic": "4.0.3",
|
||||
"@types/ws": "8.18.1",
|
||||
"@vitest/coverage-v8": "4.1.10",
|
||||
"@vitest/coverage-v8": "5.0.3",
|
||||
"ajv": "8.20.0",
|
||||
"husky": "9.1.7",
|
||||
"jscpd": "5.0.12",
|
||||
"knip": "6.26.0",
|
||||
"knip": "6.39.0",
|
||||
"lint-staged": "16.4.0",
|
||||
"oxlint": "1.73.0",
|
||||
"prettier": "3.6.2",
|
||||
"turbo": "2.10.5",
|
||||
"typedoc": "0.28.20",
|
||||
"typescript": "5.9.3",
|
||||
"vitest": "4.1.10",
|
||||
"oxfmt": "0.72.0",
|
||||
"oxlint": "1.87.0",
|
||||
"turbo": "2.11.7",
|
||||
"typescript": "7.0.2",
|
||||
"vitest": "5.0.3",
|
||||
"yaml": "2.9.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
{ "type": "generic", "path": "README_ja.md" },
|
||||
{ "type": "generic", "path": "README_ko.md" },
|
||||
{ "type": "generic", "path": "README_ar.md" },
|
||||
{ "type": "generic", "path": "docs/installation.md" },
|
||||
{
|
||||
"type": "generic",
|
||||
"path": "src/generatedPackageMetadata.ts"
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const nodeVersion = "22.19.0";
|
||||
const compiler = "x86_64-w64-mingw32-g++";
|
||||
const cache = join(homedir(), ".cache", "rea-windows-native");
|
||||
const output = join(root, "native", "windows", "build");
|
||||
const run = (command, args) => {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 4 * 1_024 * 1_024,
|
||||
timeout: 120_000,
|
||||
});
|
||||
if (result.error !== undefined)
|
||||
throw new Error(
|
||||
`Windows native build lane requires ${command}: ${result.error.message}`,
|
||||
);
|
||||
if (result.status !== 0)
|
||||
throw new Error(
|
||||
`Windows native build failed: ${result.stdout}${result.stderr}`,
|
||||
);
|
||||
return result.stdout;
|
||||
};
|
||||
if (process.platform !== "linux")
|
||||
throw new Error(
|
||||
"The Windows x64 artifact build lane currently requires Linux and MinGW-w64.",
|
||||
);
|
||||
run(compiler, ["--version"]);
|
||||
await mkdir(cache, { recursive: true });
|
||||
await mkdir(output, { recursive: true });
|
||||
const exists = async (path) =>
|
||||
access(path).then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
const fetchBytes = async (name) => {
|
||||
const response = await fetch(
|
||||
`https://nodejs.org/dist/v${nodeVersion}/${name}`,
|
||||
{
|
||||
signal: AbortSignal.timeout(60_000),
|
||||
},
|
||||
);
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
`Node build prerequisite download failed: ${name}: HTTP ${response.status}`,
|
||||
);
|
||||
return Buffer.from(await response.arrayBuffer());
|
||||
};
|
||||
const checksums = (await fetchBytes("SHASUMS256.txt")).toString("utf8");
|
||||
const verifiedDownload = async (name, path) => {
|
||||
if (!(await exists(path))) await writeFile(path, await fetchBytes(name));
|
||||
const digest = createHash("sha256")
|
||||
.update(await readFile(path))
|
||||
.digest("hex");
|
||||
const expected = checksums
|
||||
.split("\n")
|
||||
.find((line) => line.slice(66) === name)
|
||||
?.slice(0, 64);
|
||||
if (expected === undefined || digest !== expected)
|
||||
throw new Error(`Node build prerequisite SHA-256 mismatch: ${name}`);
|
||||
};
|
||||
const cachedHeaders = join(
|
||||
homedir(),
|
||||
".cache",
|
||||
"node-gyp",
|
||||
nodeVersion,
|
||||
"include",
|
||||
"node",
|
||||
);
|
||||
let headers = cachedHeaders;
|
||||
if (!(await exists(join(headers, "node_api.h")))) {
|
||||
const archive = join(cache, `node-v${nodeVersion}-headers.tar.gz`);
|
||||
await verifiedDownload(`node-v${nodeVersion}-headers.tar.gz`, archive);
|
||||
run("tar", ["xzf", archive, "-C", cache]);
|
||||
headers = join(cache, `node-v${nodeVersion}`, "include", "node");
|
||||
}
|
||||
const artifact = "rea-windows-x64.node";
|
||||
// MSVC short-import libraries do not produce a correct Node import table with
|
||||
// GNU ld. Generate a GNU import library for the exact stable Node-API symbols.
|
||||
const sources = ["addon.cc", "filesystem.cc", "process.cc"];
|
||||
const sourceText = (
|
||||
await Promise.all(
|
||||
sources.map((path) =>
|
||||
readFile(join(root, "native", "windows", "src", path), "utf8"),
|
||||
),
|
||||
)
|
||||
).join("\n");
|
||||
const symbols = [
|
||||
...new Set(
|
||||
[...sourceText.matchAll(/\b(napi_[a-z0-9_]+)\s*\(/gu)].map(
|
||||
(match) => match[1],
|
||||
),
|
||||
),
|
||||
].sort();
|
||||
const definition = join(cache, "node-api.def");
|
||||
const library = join(cache, "libnode-api.a");
|
||||
await writeFile(
|
||||
definition,
|
||||
`LIBRARY node.exe\nEXPORTS\n${symbols.join("\n")}\n`,
|
||||
);
|
||||
run("x86_64-w64-mingw32-dlltool", ["-d", definition, "-l", library]);
|
||||
run(compiler, [
|
||||
"-std=c++17",
|
||||
"-O2",
|
||||
"-DNAPI_VERSION=8",
|
||||
"-D_WIN32_WINNT=0x0A00",
|
||||
"-shared",
|
||||
"-static",
|
||||
"-static-libgcc",
|
||||
"-static-libstdc++",
|
||||
"-Wl,--no-insert-timestamp",
|
||||
`-I${headers}`,
|
||||
"native/windows/src/addon.cc",
|
||||
"native/windows/src/filesystem.cc",
|
||||
"native/windows/src/process.cc",
|
||||
library,
|
||||
"-ladvapi32",
|
||||
"-lbcrypt",
|
||||
"-lntdll",
|
||||
"-o",
|
||||
join(output, artifact),
|
||||
]);
|
||||
const packageJson = JSON.parse(
|
||||
await readFile(join(root, "package.json"), "utf8"),
|
||||
);
|
||||
const bytes = await readFile(join(output, artifact));
|
||||
const metadata = {
|
||||
packageVersion: packageJson.version,
|
||||
platform: "win32",
|
||||
architecture: "x64",
|
||||
abiVersion: 1,
|
||||
nodeApiVersion: 8,
|
||||
artifact,
|
||||
artifactSha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
};
|
||||
await writeFile(
|
||||
join(output, "manifest.json"),
|
||||
`${JSON.stringify(metadata, null, 2)}\n`,
|
||||
);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ ...metadata, artifactBytes: bytes.length })}\n`,
|
||||
);
|
||||
@@ -7,6 +7,21 @@ export async function startBrowserVerifierSite() {
|
||||
let port = 0;
|
||||
let sessionGeneration = 0;
|
||||
const server = createServer((request, response) => {
|
||||
if (request.url === "/screenshot-noise") {
|
||||
response.setHeader("content-type", "text/html");
|
||||
response.end(`<!doctype html><html><body style="margin:0"><canvas width="2048" height="1536"></canvas><script>
|
||||
const context = document.querySelector('canvas').getContext('2d');
|
||||
const image = context.createImageData(2048, 1536);
|
||||
let state = 0x6d2b79f5;
|
||||
for (let index = 0; index < image.data.length; index++) {
|
||||
state ^= state << 13; state ^= state >>> 17; state ^= state << 5;
|
||||
image.data[index] = index % 4 === 3 ? 255 : state & 255;
|
||||
}
|
||||
context.putImageData(image, 0, 0);
|
||||
document.body.dataset.ready = 'true';
|
||||
</script></body></html>`);
|
||||
return;
|
||||
}
|
||||
if (request.url === "/app.js.map") {
|
||||
response.setHeader("content-type", "application/source-map+json");
|
||||
response.end(sourceMap());
|
||||
@@ -102,7 +117,9 @@ const browserScript = (port) => `
|
||||
const { generation } = await response.json();
|
||||
if (generation <= observedGeneration) return;
|
||||
observedGeneration = generation;
|
||||
history.pushState({}, "", "/app/session-" + String(generation));
|
||||
const sessionUrl = new URL(window.location.href);
|
||||
sessionUrl.pathname = "/app/session-" + String(generation);
|
||||
history.pushState({}, "", sessionUrl);
|
||||
}, 100);
|
||||
//# sourceMappingURL=/app.js.map
|
||||
`;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { analysisErrorJsonSchema } from "../dist/contracts/errorSchemas.js";
|
||||
import { format } from "prettier";
|
||||
import { formatGeneratedFile } from "./lib/format-generated-file.mjs";
|
||||
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
|
||||
|
||||
const arguments_ = new Set(process.argv.slice(2));
|
||||
@@ -13,9 +13,10 @@ for (const argument of arguments_)
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
await ensureGeneratedFile({
|
||||
path: join(root, "docs/error-contract.schema.json"),
|
||||
source: await format(JSON.stringify(analysisErrorJsonSchema), {
|
||||
parser: "json",
|
||||
}),
|
||||
source: await formatGeneratedFile(
|
||||
"docs/error-contract.schema.json",
|
||||
JSON.stringify(analysisErrorJsonSchema),
|
||||
),
|
||||
check: arguments_.has("--check"),
|
||||
generateCommand: "npm run docs:generate",
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { format } from "prettier";
|
||||
import { formatGeneratedFile } from "./lib/format-generated-file.mjs";
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { McpServer } from "@modelcontextprotocol/server";
|
||||
|
||||
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
|
||||
@@ -28,6 +29,16 @@ const sessionToolNames = new Set([
|
||||
...MANAGED_WORKFLOW_TOOL_CONTRACTS.map(({ name }) => name),
|
||||
]);
|
||||
const advertisedTools = await sdkToolCatalog();
|
||||
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
||||
for (const tool of advertisedTools.values()) {
|
||||
for (const kind of ["inputSchema", "outputSchema"]) {
|
||||
const schema = tool[kind];
|
||||
if (schema !== undefined && !ajv.validateSchema(schema))
|
||||
throw new Error(
|
||||
`Invalid JSON Schema for ${tool.name}.${kind}: ${ajv.errorsText(ajv.errors)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const catalog = TOOL_CONTRACTS.map((contract) => {
|
||||
const advertised = advertisedTools.get(contract.name);
|
||||
if (advertised === undefined)
|
||||
@@ -78,7 +89,7 @@ async function sdkToolCatalog() {
|
||||
}
|
||||
}
|
||||
const auxiliaryProviders = [
|
||||
new ArtifactProvider(false, false),
|
||||
new ArtifactProvider(CATALOG_PLATFORM),
|
||||
new NativeMacOSProvider(undefined, CATALOG_PLATFORM),
|
||||
new ManagedStaticProvider(),
|
||||
].map((provider) => ({
|
||||
@@ -98,8 +109,11 @@ const canonicalizeJson = (value) => {
|
||||
|
||||
const payloadJson = JSON.stringify(
|
||||
canonicalizeJson({ catalog, auxiliaryProviders }),
|
||||
null,
|
||||
2,
|
||||
);
|
||||
const source = await format(
|
||||
const source = await formatGeneratedFile(
|
||||
"src/generatedMcpToolCatalog.ts",
|
||||
`import type { ToolAnnotations } from "@modelcontextprotocol/server";
|
||||
import type {
|
||||
CapabilityDescriptor,
|
||||
@@ -183,7 +197,6 @@ function isRecord(
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
`,
|
||||
{ parser: "typescript" },
|
||||
);
|
||||
|
||||
await ensureGeneratedFile({
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
|
||||
const environment = () => ({
|
||||
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_ANALYSIS_PROVIDER: "auto",
|
||||
// Artifact workflows have no deep-provider prerequisite or GUI authority.
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
|
||||
});
|
||||
|
||||
/** Execute the compiled CLI with production providers and no test adapters. */
|
||||
export async function artifactCli(command, target, arguments_ = []) {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[
|
||||
fileURLToPath(new URL("../rea.mjs", import.meta.url)),
|
||||
command,
|
||||
target,
|
||||
...arguments_,
|
||||
"--json",
|
||||
],
|
||||
{ env: environment(), timeout: 60000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const evidence = JSON.parse(stdout);
|
||||
assert.equal(evidence.error, undefined, stdout);
|
||||
assert.ok(evidence.normalized_result, "CLI omitted normalized result");
|
||||
return evidence.normalized_result;
|
||||
}
|
||||
|
||||
/** Run a real stdio MCP subprocess, owning and closing its transport. */
|
||||
export async function withArtifactMcp(target, verify) {
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [fileURLToPath(new URL("../rea.mjs", import.meta.url)), "mcp"],
|
||||
env: environment(),
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "artifact-real-e2e", version: "1" });
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const opened = await client.callTool({
|
||||
name: "open_binary",
|
||||
arguments: { path: target },
|
||||
});
|
||||
assert.notEqual(opened.isError, true, JSON.stringify(opened));
|
||||
await verify(client);
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** Require a successful structured MCP tool result rather than its text rendering. */
|
||||
export async function artifactMcpResult(client, name, arguments_ = {}) {
|
||||
const result = await client.callTool({ name, arguments: arguments_ });
|
||||
assert.notEqual(result.isError, true, JSON.stringify(result));
|
||||
assert.ok(result.structuredContent?.result, "MCP omitted structured result");
|
||||
return result.structuredContent.result;
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { once } from "node:events";
|
||||
import { createServer } from "node:http";
|
||||
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 { PlaywrightBrowserScenarioProvider } from "../../dist/browser/PlaywrightBrowserScenarioProvider.js";
|
||||
import { PlaywrightScenarioEvents } from "../../dist/browser/PlaywrightScenarioEvents.js";
|
||||
import { BrowserScenarioSecrets } from "../../dist/browser/BrowserScenarioSecrets.js";
|
||||
import { browserScenarioSchema } from "../../dist/domain/browserScenario.js";
|
||||
import { compareBrowserScenarios } from "../../dist/domain/browserScenarioDiff.js";
|
||||
import { browserScenarioCaptureSchema } from "../../dist/domain/browserScenarioCapture.js";
|
||||
import { parseEvidence } from "../../dist/domain/evidence.js";
|
||||
|
||||
const document = (path) => {
|
||||
if (path.startsWith("/nested"))
|
||||
return `<script>throw new Error("nested popup failure")</script>
|
||||
<script>opener.postMessage("nested-ready", "*")</script>`;
|
||||
if (path.startsWith("/popup"))
|
||||
return `<script>throw new Error("popup failure")</script>
|
||||
<script>onmessage = e => {
|
||||
if (e.data === "nested-ready") opener.postMessage("popup-ready", "*");
|
||||
}; window.open("/nested");</script><iframe src="/child"></iframe>`;
|
||||
if (path.startsWith("/child")) return "child frame";
|
||||
return `<script>onmessage = e => {
|
||||
if (e.data === "popup-ready") document.body.dataset.ready = "true";
|
||||
}</script><button onclick="window.open('/redirect')">Open popup</button>`;
|
||||
};
|
||||
|
||||
const scenarioFor = (executable, origin, events) =>
|
||||
browserScenarioSchema.parse({
|
||||
browser: { mode: "launch", executable_path: executable },
|
||||
start_url: { url: origin },
|
||||
actions: [
|
||||
{
|
||||
step_id: "open",
|
||||
action: "click",
|
||||
locator: { kind: "role", role: "button", name: "Open popup" },
|
||||
},
|
||||
{
|
||||
step_id: "ready",
|
||||
action: "wait_for",
|
||||
locator: { kind: "css", selector: "body[data-ready=true]" },
|
||||
state: "attached",
|
||||
timeout_ms: 10_000,
|
||||
},
|
||||
],
|
||||
capture: { events, after_each_step: [], at_end: [] },
|
||||
});
|
||||
|
||||
const capture = async (scenario) => {
|
||||
const result = await new PlaywrightBrowserScenarioProvider().captureScenario(
|
||||
scenario,
|
||||
);
|
||||
if (!result.ok) throw result.error;
|
||||
assert.ok(
|
||||
result.value.steps.every((step) => step.status === "completed"),
|
||||
JSON.stringify(result.value.steps),
|
||||
);
|
||||
return result.value;
|
||||
};
|
||||
|
||||
const assertNetwork = (result, origin) => {
|
||||
for (const path of ["/redirect", "/popup", "/nested"])
|
||||
for (const kind of ["request", "response"])
|
||||
assert.equal(
|
||||
result.events.items.filter(
|
||||
(event) => event.kind === kind && event.url?.url === origin + path,
|
||||
).length,
|
||||
1,
|
||||
`${kind} ${path} must be captured exactly once`,
|
||||
);
|
||||
assert.equal(result.completeness.equality_eligible, true);
|
||||
assert.equal(compareWithSelf(result).overall_status, "unchanged");
|
||||
};
|
||||
|
||||
const compareWithSelf = (capture) =>
|
||||
compareBrowserScenarios({
|
||||
before_scenario: capture,
|
||||
after_scenario: capture,
|
||||
normalization: { rules: [] },
|
||||
});
|
||||
|
||||
const verifySharedContext = async (executable, origin) => {
|
||||
const browser = await chromium.launch({
|
||||
executablePath: executable,
|
||||
headless: true,
|
||||
});
|
||||
try {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
const foreign = await context.newPage();
|
||||
const secrets = BrowserScenarioSecrets.resolve(
|
||||
scenarioFor(executable, origin, ["network"]),
|
||||
{},
|
||||
);
|
||||
assert.ok(secrets);
|
||||
const events = new PlaywrightScenarioEvents({
|
||||
page,
|
||||
context,
|
||||
ownsContext: false,
|
||||
enabled: new Set(["network"]),
|
||||
secrets,
|
||||
});
|
||||
await Promise.all([
|
||||
page.goto(origin),
|
||||
foreign.goto(origin + "/popup?unrelated-tab"),
|
||||
]);
|
||||
await page.getByRole("button", { name: "Open popup", exact: true }).click();
|
||||
await page.locator("body[data-ready=true]").waitFor({ timeout: 10_000 });
|
||||
const retained = events.result().items;
|
||||
assert.ok(
|
||||
events
|
||||
.limitations()
|
||||
.some((value) => value.includes("Shared-context network")),
|
||||
);
|
||||
assert.ok(retained.some((event) => event.url?.url === origin + "/"));
|
||||
assert.ok(!JSON.stringify(retained).includes("unrelated-tab"));
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
};
|
||||
|
||||
const verifyTransports = async (executable, origin) => {
|
||||
const scenario = scenarioFor(executable, origin, ["network"]);
|
||||
const entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
|
||||
const env = {
|
||||
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
||||
REA_LOG_LEVEL: "silent",
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
|
||||
};
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"capture-browser-scenario",
|
||||
JSON.stringify(scenario),
|
||||
"--json",
|
||||
],
|
||||
{ env, timeout: 60_000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
assertNetwork(
|
||||
browserScenarioCaptureSchema.parse(
|
||||
parseEvidence(JSON.parse(stdout)).normalized_result,
|
||||
),
|
||||
origin,
|
||||
);
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "popup-real-e2e", version: "1" });
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const captured = await client.callTool(
|
||||
{ name: "capture_browser_scenario", arguments: scenario },
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
assert.notEqual(captured.isError, true, JSON.stringify(captured));
|
||||
const evidence = parseEvidence({
|
||||
...captured.structuredContent?.evidence,
|
||||
normalized_result: captured.structuredContent?.result,
|
||||
});
|
||||
assertNetwork(
|
||||
browserScenarioCaptureSchema.parse(evidence.normalized_result),
|
||||
origin,
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
await client.close();
|
||||
} finally {
|
||||
await transport.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** Verify popup timing, selection, coverage, and shared-context isolation on Chromium. */
|
||||
export async function verifyPopupEventCoverage(executable) {
|
||||
const server = createServer((request, response) => {
|
||||
if (request.url === "/redirect") {
|
||||
response.writeHead(302, { Location: "/popup" });
|
||||
response.end();
|
||||
} else {
|
||||
response.setHeader("Content-Type", "text/html");
|
||||
response.end(document(request.url ?? "/"));
|
||||
}
|
||||
});
|
||||
server.listen(0, "127.0.0.1");
|
||||
await once(server, "listening");
|
||||
try {
|
||||
const address = server.address();
|
||||
assert.ok(address !== null && typeof address === "object");
|
||||
const origin = `http://127.0.0.1:${address.port}`;
|
||||
assertNetwork(
|
||||
await capture(scenarioFor(executable, origin, ["network"])),
|
||||
origin,
|
||||
);
|
||||
const errors = await capture(
|
||||
scenarioFor(executable, origin, ["page-errors"]),
|
||||
);
|
||||
assert.deepEqual(
|
||||
errors.events.items.map((event) => event.kind),
|
||||
["page-error", "page-error"],
|
||||
);
|
||||
assert.ok(
|
||||
errors.events.items.some((event) => event.message === "popup failure"),
|
||||
);
|
||||
assert.ok(
|
||||
errors.events.items.some(
|
||||
(event) => event.message === "nested popup failure",
|
||||
),
|
||||
);
|
||||
for (const family of ["frames", "workers", "websockets", "downloads"]) {
|
||||
const result = await capture(scenarioFor(executable, origin, [family]));
|
||||
assert.equal(result.completeness.equality_eligible, false);
|
||||
assert.ok(result.completeness.missing_sections.includes("events"));
|
||||
assert.equal(compareWithSelf(result).overall_status, "unknown");
|
||||
assert.ok(
|
||||
result.limitations.some((limitation) =>
|
||||
limitation.includes(`Popup ${family}`),
|
||||
),
|
||||
);
|
||||
}
|
||||
await verifySharedContext(executable, origin);
|
||||
await verifyTransports(executable, origin);
|
||||
return {
|
||||
mocked: false,
|
||||
initial_network: true,
|
||||
redirects: true,
|
||||
nested_popups: true,
|
||||
unselected_lifecycle_absent: true,
|
||||
popup_event_gaps_explicit: true,
|
||||
incomplete_comparison_unknown: true,
|
||||
shared_context_isolation: true,
|
||||
cli: true,
|
||||
stdio_mcp: true,
|
||||
};
|
||||
} finally {
|
||||
server.closeAllConnections();
|
||||
await new Promise((resolve, reject) =>
|
||||
server.close((error) =>
|
||||
error === undefined ? resolve() : reject(error),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { readdir } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { browserScenarioSchema } from "../../dist/domain/browserScenario.js";
|
||||
@@ -18,7 +17,6 @@ const duplicateOriginStorage = (origin) => ({
|
||||
value: {
|
||||
source: "literal",
|
||||
value: "first",
|
||||
classification: "public",
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -31,9 +29,12 @@ const duplicateOriginStorage = (origin) => ({
|
||||
value: {
|
||||
source: "literal",
|
||||
value: "second",
|
||||
classification: "public",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "rea-secret-seed",
|
||||
value: { source: "secret", secret_id: "verifier_storage" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -43,15 +44,21 @@ const duplicateOriginStorage = (origin) => ({
|
||||
export function browserScenario(browser, origin) {
|
||||
return browserScenarioSchema.parse({
|
||||
browser,
|
||||
start_url: { url: `${origin}/app`, query: [] },
|
||||
allowed_origins: [origin],
|
||||
start_url: {
|
||||
url: `${origin}/app`,
|
||||
query: [
|
||||
{
|
||||
name: "scenario_token",
|
||||
value: { source: "secret", secret_id: "verifier_url" },
|
||||
},
|
||||
],
|
||||
},
|
||||
environment: {
|
||||
viewport: { width: 1_280, height: 720, device_scale_factor: 1.25 },
|
||||
locale: "en-US",
|
||||
timezone: "UTC",
|
||||
color_scheme: "light",
|
||||
reduced_motion: "reduce",
|
||||
service_workers: "block",
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
@@ -61,7 +68,6 @@ export function browserScenario(browser, origin) {
|
||||
kind: "role",
|
||||
role: "button",
|
||||
name: "ax-private-label-value",
|
||||
exact: true,
|
||||
},
|
||||
state: "visible",
|
||||
},
|
||||
@@ -72,17 +78,20 @@ export function browserScenario(browser, origin) {
|
||||
kind: "role",
|
||||
role: "button",
|
||||
name: "ax-private-label-value",
|
||||
exact: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
storage: duplicateOriginStorage(origin),
|
||||
request_replay: { mode: "disabled" },
|
||||
secrets: [],
|
||||
redaction: {
|
||||
secret_values: "replace-with-secret-reference",
|
||||
query_parameter_names: ["token"],
|
||||
},
|
||||
secrets: [
|
||||
{
|
||||
secret_id: "verifier_url",
|
||||
environment_variable: "REA_BROWSER_VERIFIER_URL",
|
||||
},
|
||||
{
|
||||
secret_id: "verifier_storage",
|
||||
environment_variable: "REA_BROWSER_VERIFIER_SECRET",
|
||||
},
|
||||
],
|
||||
capture: {
|
||||
after_each_step: [
|
||||
"screenshot",
|
||||
@@ -115,7 +124,7 @@ export function browserScenario(browser, origin) {
|
||||
}
|
||||
|
||||
/** Run scenario capture through the public one-shot CLI. */
|
||||
export async function runScenarioCli(scenario, configuration) {
|
||||
export async function runScenarioCli(scenario) {
|
||||
const { stdout } = await execute(
|
||||
process.execPath,
|
||||
[
|
||||
@@ -126,16 +135,7 @@ export async function runScenarioCli(scenario, configuration) {
|
||||
],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
REA_BROWSER_SCENARIO_ENABLED: "true",
|
||||
REA_BROWSER_SCENARIO_AUTO_GRANT: "true",
|
||||
REA_BROWSER_SCENARIO_EXECUTABLE_ROOTS_JSON: JSON.stringify([
|
||||
dirname(scenario.browser.executable_path ?? process.execPath),
|
||||
]),
|
||||
REA_BROWSER_SCENARIO_ALLOWED_ENV_JSON: "[]",
|
||||
...configuration,
|
||||
},
|
||||
env: process.env,
|
||||
maxBuffer: 64 * 1_024 * 1_024,
|
||||
},
|
||||
);
|
||||
@@ -175,6 +175,35 @@ export function assertScenarioCapture(capture) {
|
||||
!storageNames.includes("rea-second-seed")
|
||||
)
|
||||
throw new Error("Scenario launch did not retain duplicate-origin seeds");
|
||||
const secretSeed =
|
||||
final.artifacts.storage.state === "captured"
|
||||
? final.artifacts.storage.value.local_storage.find(
|
||||
({ name }) => name === "rea-secret-seed",
|
||||
)
|
||||
: undefined;
|
||||
if (secretSeed?.value_state !== "redacted-secret")
|
||||
throw new Error("Scenario launch did not redact its declared secret seed");
|
||||
const finalUrl =
|
||||
final.artifacts.url.state === "captured"
|
||||
? final.artifacts.url.value.url
|
||||
: "";
|
||||
if (!finalUrl.includes("[REDACTED:verifier_url]"))
|
||||
throw new Error("Scenario launch did not redact its declared URL secret");
|
||||
const stepUrl = capture.steps
|
||||
.map(({ after_url }) => after_url)
|
||||
.find(({ url }) => url.includes("[REDACTED:verifier_url]"));
|
||||
if (stepUrl?.redacted !== true)
|
||||
throw new Error(
|
||||
"Scenario step URL did not report declared-secret redaction",
|
||||
);
|
||||
const eventUrl = capture.events.items
|
||||
.filter((event) => "url" in event && event.url !== null)
|
||||
.map((event) => event.url)
|
||||
.find(({ url }) => url.includes("[REDACTED:verifier_url]"));
|
||||
if (eventUrl?.redacted !== true)
|
||||
throw new Error(
|
||||
"Scenario event URL did not report declared-secret redaction",
|
||||
);
|
||||
if (
|
||||
capture.events.items.length === 0 ||
|
||||
capture.completeness.equality_eligible !== true
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { chromium } from "playwright-core";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { parseEvidence } from "../../dist/domain/evidence.js";
|
||||
import { webScreenshotSchema } from "../../dist/domain/webScreenshot.js";
|
||||
import { comparePngScreenshots } from "../../dist/browser/PngVisualDiff.js";
|
||||
|
||||
/** Verify a real, large PNG through production CLI and stdio MCP boundaries. */
|
||||
export async function verifyLargeScreenshotE2e(endpoint, origin) {
|
||||
const browser = await chromium.connectOverCDP(endpoint);
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 2048, height: 1536 },
|
||||
deviceScaleFactor: 1,
|
||||
});
|
||||
const entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
|
||||
const env = {
|
||||
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
||||
REA_LOG_LEVEL: "silent",
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
|
||||
};
|
||||
let client;
|
||||
let transport;
|
||||
try {
|
||||
const page = await context.newPage();
|
||||
await page.goto(`${origin}/screenshot-noise`);
|
||||
await page.waitForFunction(() => document.body.dataset.ready === "true");
|
||||
const targets = await (await fetch(`${endpoint}/json/list`)).json();
|
||||
const target = targets.find((value) => value.url === page.url());
|
||||
assert.ok(
|
||||
target,
|
||||
"Owned screenshot page is absent from actual CDP discovery",
|
||||
);
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"capture-web-screenshot",
|
||||
endpoint,
|
||||
target.id,
|
||||
"--allowed-origins",
|
||||
origin,
|
||||
"--json",
|
||||
],
|
||||
{ env, timeout: 60000, maxBuffer: 72 * 1024 * 1024 },
|
||||
);
|
||||
const cliEvidence = parseEvidence(JSON.parse(stdout));
|
||||
assert.equal(cliEvidence.operation, "capture_web_screenshot");
|
||||
const screenshot = webScreenshotSchema.parse(cliEvidence.normalized_result);
|
||||
assert.ok(
|
||||
screenshot.artifact.bytes > 8 * 1024 * 1024,
|
||||
"Real PNG did not cross the former screenshot byte ceiling",
|
||||
);
|
||||
const bytes = Buffer.from(screenshot.artifact.data_base64, "base64");
|
||||
assert.equal(bytes.byteLength, screenshot.artifact.bytes);
|
||||
assert.deepEqual(
|
||||
bytes.subarray(0, 8),
|
||||
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
|
||||
);
|
||||
assert.equal(bytes.readUInt32BE(16), 2048);
|
||||
assert.equal(bytes.readUInt32BE(20), 1536);
|
||||
transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
// The SDK defaults to a 10 MiB receive buffer; inline PNG output is larger.
|
||||
maxBufferSize: 64 * 1024 * 1024,
|
||||
});
|
||||
client = new Client({ name: "browser-screenshot-real-e2e", version: "1" });
|
||||
await client.connect(transport);
|
||||
const captured = await client.callTool(
|
||||
{
|
||||
name: "capture_web_screenshot",
|
||||
arguments: {
|
||||
cdp_endpoint: endpoint,
|
||||
target_id: target.id,
|
||||
},
|
||||
},
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
assert.notEqual(captured.isError, true, JSON.stringify(captured));
|
||||
const mcpEvidence = parseEvidence({
|
||||
...captured.structuredContent?.evidence,
|
||||
normalized_result: captured.structuredContent?.result,
|
||||
});
|
||||
const mcpScreenshot = webScreenshotSchema.parse(
|
||||
mcpEvidence.normalized_result,
|
||||
);
|
||||
assert.deepEqual(mcpScreenshot.artifact, screenshot.artifact);
|
||||
const compared = comparePngScreenshots({
|
||||
before: screenshot.artifact,
|
||||
after: mcpScreenshot.artifact,
|
||||
channel_threshold: 0,
|
||||
});
|
||||
assert.equal(compared.status, "identical");
|
||||
assert.equal(compared.changed_pixels, 0);
|
||||
return {
|
||||
mocked: false,
|
||||
cli: true,
|
||||
stdio_mcp: true,
|
||||
png_bytes: bytes.byteLength,
|
||||
};
|
||||
} finally {
|
||||
try {
|
||||
if (client !== undefined) await client.close();
|
||||
} finally {
|
||||
try {
|
||||
if (transport !== undefined) await transport.close();
|
||||
} finally {
|
||||
await context.close();
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,7 +133,7 @@ export function assertBundleAnalysis(result) {
|
||||
);
|
||||
}
|
||||
|
||||
/** Assert explicitly approved console, JSON, and WebSocket shape capture. */
|
||||
/** Assert explicitly selected console text and JSON/WebSocket shape capture. */
|
||||
export function assertSensitiveShapes(result) {
|
||||
const request = result.network.requests.find(
|
||||
(item) =>
|
||||
@@ -151,12 +151,11 @@ export function assertSensitiveShapes(result) {
|
||||
(event) => event.text_capture.values,
|
||||
);
|
||||
if (
|
||||
!consoleText.some(({ text }) => text.includes("[REDACTED]")) ||
|
||||
consoleText.some(({ text }) => text.includes("console-secret-value"))
|
||||
!consoleText.some(({ text }) =>
|
||||
text.includes("authorization=Bearer console-secret-value"),
|
||||
)
|
||||
)
|
||||
throw new Error(
|
||||
"Real Chrome approved console text was not safely redacted",
|
||||
);
|
||||
throw new Error("Real Chrome selected console text was not preserved");
|
||||
if (
|
||||
!result.network.websocket_events.some(
|
||||
(event) =>
|
||||
@@ -174,8 +173,8 @@ export function assertSensitiveShapes(result) {
|
||||
for (const secret of [
|
||||
"request-body-secret-value",
|
||||
"response-secret-value",
|
||||
"request-secret-value",
|
||||
"websocket-secret-value",
|
||||
"console-secret-value",
|
||||
])
|
||||
if (serialized.includes(secret))
|
||||
throw new Error(`Approved shape capture retained raw value: ${secret}`);
|
||||
|
||||
@@ -6,12 +6,12 @@ export const toolFamilyCatalog = (sources) => {
|
||||
{
|
||||
id: "direct",
|
||||
surface: "official-proxy",
|
||||
contracts: sources.toolContracts.OFFICIAL_TOOL_CONTRACTS,
|
||||
contracts: sources.officialContracts.OFFICIAL_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
id: "enhanced",
|
||||
surface: "enhanced",
|
||||
contracts: sources.toolContracts.ENHANCED_TOOL_CONTRACTS,
|
||||
contracts: sources.enhancedContracts.ENHANCED_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
id: "native",
|
||||
@@ -59,7 +59,7 @@ export const toolFamilyCatalog = (sources) => {
|
||||
{
|
||||
id: "session",
|
||||
surface: "session",
|
||||
contracts: sources.toolContracts.SESSION_TOOL_CONTRACTS,
|
||||
contracts: sources.sessionContracts.SESSION_TOOL_CONTRACTS,
|
||||
},
|
||||
].map(({ id, surface, contracts }) => ({
|
||||
id,
|
||||
|
||||
@@ -37,6 +37,9 @@ const SOURCE_PATHS = {
|
||||
catalogIdentity: "dist/catalogIdentity.js",
|
||||
cli: "dist/cli.js",
|
||||
toolContracts: "dist/contracts/toolContracts.js",
|
||||
officialContracts: "dist/contracts/officialToolContracts.js",
|
||||
enhancedContracts: "dist/contracts/enhancedToolContracts.js",
|
||||
sessionContracts: "dist/contracts/sessionToolContracts.js",
|
||||
nativeContracts: "dist/contracts/nativeToolContracts.js",
|
||||
artifactContracts: "dist/contracts/artifactToolContracts.js",
|
||||
managedContracts: "dist/contracts/managedToolContracts.js",
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export interface DevelopmentTestRequest {
|
||||
readonly mode: "local" | "changed" | "focused";
|
||||
readonly paths: readonly string[];
|
||||
readonly base: string;
|
||||
readonly dryRun: boolean;
|
||||
}
|
||||
|
||||
/** Parse explicit test paths and an optional branch-comparison base. */
|
||||
export function parseDevelopmentTestRequest(
|
||||
mode: string,
|
||||
arguments_: readonly string[],
|
||||
): DevelopmentTestRequest;
|
||||
|
||||
/** Plan source feedback or a strict explicit run with its build prerequisite. */
|
||||
export function developmentTestPlan(
|
||||
request: DevelopmentTestRequest,
|
||||
baseCommit?: string,
|
||||
): {
|
||||
readonly needsBuild: boolean;
|
||||
readonly vitestArguments: readonly string[];
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
const sourceProjects = [
|
||||
"domain",
|
||||
"services",
|
||||
"adapters",
|
||||
"composition",
|
||||
"conformance",
|
||||
"evaluation",
|
||||
];
|
||||
|
||||
const sourceTest = (path) =>
|
||||
path.startsWith("src/") ||
|
||||
["composition", "conformance", "evaluation"].some((project) =>
|
||||
path.startsWith(`tests/${project}/`),
|
||||
);
|
||||
|
||||
/** Parse explicit test paths and an optional branch-comparison base. */
|
||||
export const parseDevelopmentTestRequest = (mode, arguments_) => {
|
||||
if (!["local", "changed", "focused"].includes(mode))
|
||||
throw new Error("Expected local, changed, or focused test mode");
|
||||
const paths = [];
|
||||
let base = "origin/main";
|
||||
let dryRun = false;
|
||||
for (let index = 0; index < arguments_.length; index += 1) {
|
||||
const argument = arguments_[index];
|
||||
if (argument === "--dry-run") dryRun = true;
|
||||
else if (argument === "--base" || argument.startsWith("--base=")) {
|
||||
if (mode !== "changed") throw new Error("--base requires changed mode");
|
||||
base = argument === "--base" ? arguments_[++index] : argument.slice(7);
|
||||
if (!base || base.startsWith("-") || base.includes("\0"))
|
||||
throw new Error("--base requires a Git revision");
|
||||
} else {
|
||||
const path = argument.replaceAll("\\", "/").replace(/^\.\//u, "");
|
||||
if (
|
||||
!/^(?:src|tests)\/.+\.test\.ts$/u.test(path) ||
|
||||
path.split("/").some((part) => part === ".." || part === ".")
|
||||
)
|
||||
throw new Error(`Expected an exact repository test path: ${argument}`);
|
||||
if (mode !== "focused" && !sourceTest(path))
|
||||
throw new Error(
|
||||
`Use test:focused for compiled boundary tests: ${path}`,
|
||||
);
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
if (mode === "focused" && paths.length === 0)
|
||||
throw new Error("test:focused requires at least one exact test path");
|
||||
if (mode === "changed" && paths.length > 0)
|
||||
throw new Error("Use test:focused for explicit test paths");
|
||||
return { mode, paths: [...new Set(paths)], base, dryRun };
|
||||
};
|
||||
|
||||
/** Plan source feedback or a strict explicit run with its build prerequisite. */
|
||||
export const developmentTestPlan = (request, baseCommit) => {
|
||||
const explicit = request.paths.length > 0;
|
||||
if (
|
||||
request.mode === "changed" &&
|
||||
!/^[a-f0-9]{40,64}$/u.test(baseCommit ?? "")
|
||||
)
|
||||
throw new Error("Changed tests require a resolved Git merge base");
|
||||
const projects = request.mode === "focused" ? [] : sourceProjects;
|
||||
return {
|
||||
needsBuild: request.paths.some((path) => !sourceTest(path)),
|
||||
vitestArguments: [
|
||||
"run",
|
||||
...projects.flatMap((project) => ["--project", project]),
|
||||
...(explicit
|
||||
? request.paths
|
||||
: [
|
||||
request.mode === "changed"
|
||||
? `--changed=${baseCommit}`
|
||||
: "--changed",
|
||||
"--passWithNoTests",
|
||||
]),
|
||||
],
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { format } from "oxfmt";
|
||||
import config from "../../.oxfmtrc.json" with { type: "json" };
|
||||
|
||||
/** Format generated source with the same options as the repository formatter. */
|
||||
export const formatGeneratedFile = async (fileName, source) => {
|
||||
const result = await format(fileName, source, config);
|
||||
if (result.errors.length > 0)
|
||||
throw new Error(
|
||||
`Cannot format ${fileName}: ${result.errors.map(({ message }) => message).join("; ")}`,
|
||||
);
|
||||
return result.code;
|
||||
};
|
||||
@@ -93,11 +93,6 @@ const sourceOwnedClaims = ({
|
||||
scenarioId: "decompiler-reconstruction-import",
|
||||
targets: [obfuscated.target],
|
||||
}),
|
||||
completionClaim({
|
||||
claimId: "managed.runtime-correlation-admission-plan",
|
||||
scenarioId: "runtime-correlation-admission-plan",
|
||||
targets: [obfuscated.target],
|
||||
}),
|
||||
completionClaim({
|
||||
claimId: "managed.mvid-and-token-drift",
|
||||
scenarioId: "mvid-and-token-drift",
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
|
||||
import { parseEvidence } from "../../dist/domain/evidence.js";
|
||||
import { analysisProfileSchema } from "../../dist/domain/analysisProfile.js";
|
||||
import { nativeValueTraceSchema } from "../../dist/domain/nativeValueTrace.js";
|
||||
|
||||
const defaultEntrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
|
||||
const environment = () => ({
|
||||
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_ANALYSIS_PROVIDER: "ghidra",
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
|
||||
...(process.env.GHIDRA_INSTALL_DIR === undefined
|
||||
? {}
|
||||
: { GHIDRA_INSTALL_DIR: process.env.GHIDRA_INSTALL_DIR }),
|
||||
...(process.env.JAVA_HOME === undefined
|
||||
? {}
|
||||
: { JAVA_HOME: process.env.JAVA_HOME }),
|
||||
});
|
||||
|
||||
const verifyGraph = (input, target, procedure, globalAddress) => {
|
||||
const graph = nativeValueTraceSchema.parse(input);
|
||||
assert.equal(graph.target_sha256, target.sha256);
|
||||
assert.equal(graph.seed, procedure);
|
||||
const nodes = new Map(graph.nodes.map((node) => [node.id, node]));
|
||||
for (const kind of ["argument-binding", "parameter-use", "return-binding"]) {
|
||||
const edges = graph.edges.filter((edge) => edge.kind === kind);
|
||||
assert.ok(edges.length > 0, `Real native dependency graph lacks ${kind}`);
|
||||
for (const edge of edges) {
|
||||
const source = nodes.get(edge.source);
|
||||
const destination = nodes.get(edge.target);
|
||||
assert.ok(source, `Missing source node ${edge.source}`);
|
||||
assert.ok(destination, `Missing target node ${edge.target}`);
|
||||
assert.equal(edge.status, "derived");
|
||||
if (kind === "argument-binding") {
|
||||
assert.match(source.operation?.opcode ?? "", /^CALL(?:IND)?$/u);
|
||||
assert.equal(destination.kind, "parameter");
|
||||
assert.equal(edge.input_index, destination.parameter.ordinal + 1);
|
||||
} else if (kind === "parameter-use") {
|
||||
assert.equal(source.kind, "parameter");
|
||||
assert.ok(destination.operation.inputs[edge.input_index]);
|
||||
} else {
|
||||
assert.equal(source.operation?.opcode, "RETURN");
|
||||
assert.match(destination.operation?.opcode ?? "", /^CALL(?:IND)?$/u);
|
||||
assert.equal(edge.input_index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.ok(
|
||||
graph.nodes.some((node) =>
|
||||
node.operation?.inputs.some((value) => value.location === globalAddress),
|
||||
),
|
||||
"Source-owned global operand is absent from the real dependency graph",
|
||||
);
|
||||
for (const node of graph.nodes)
|
||||
assert.match(node.evidence_id, /^ev_[a-f0-9]{64}$/u);
|
||||
for (const procedure of graph.procedures) {
|
||||
assert.equal(procedure.provider, "ghidra");
|
||||
assert.match(procedure.analysis_profile_digest, /^[a-f0-9]{64}$/u);
|
||||
}
|
||||
return graph;
|
||||
};
|
||||
|
||||
/** Verify real Ghidra dependency composition through production CLI and stdio MCP. */
|
||||
export async function verifyNativeValueE2e(
|
||||
target,
|
||||
procedure,
|
||||
globalAddress,
|
||||
{ entrypoint = defaultEntrypoint } = {},
|
||||
) {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"trace-native-values",
|
||||
target.path,
|
||||
procedure,
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--max-depth",
|
||||
"2",
|
||||
"--max-functions",
|
||||
"8",
|
||||
"--limit",
|
||||
"5000",
|
||||
"--json",
|
||||
],
|
||||
{ env: environment(), timeout: 240000, maxBuffer: 72 * 1024 * 1024 },
|
||||
);
|
||||
const evidence = parseEvidence(JSON.parse(stdout));
|
||||
assert.equal(evidence.operation, "trace_native_values");
|
||||
assert.equal(evidence.provider.id, "rea-workflow");
|
||||
const workflowProfile = analysisProfileSchema.parse(
|
||||
evidence.analysis_profile,
|
||||
);
|
||||
assert.equal(workflowProfile.provider.id, "rea-workflow");
|
||||
const upstreamProfile = analysisProfileSchema.parse(
|
||||
workflowProfile.parameters.upstream_analysis_profile,
|
||||
);
|
||||
assert.equal(upstreamProfile.provider.id, "ghidra");
|
||||
const cliGraph = verifyGraph(
|
||||
evidence.normalized_result,
|
||||
target,
|
||||
procedure,
|
||||
globalAddress,
|
||||
);
|
||||
for (const procedure of cliGraph.procedures)
|
||||
assert.equal(procedure.analysis_profile_digest, upstreamProfile.digest);
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env: environment(),
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "native-values-real-e2e", version: "1" });
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const opened = await client.callTool(
|
||||
{
|
||||
name: "open_binary",
|
||||
arguments: { path: target.path, provider: "ghidra" },
|
||||
},
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
assert.notEqual(opened.isError, true, JSON.stringify(opened));
|
||||
const inventory = await client.callTool({
|
||||
name: "binary_session",
|
||||
arguments: {},
|
||||
});
|
||||
assert.notEqual(inventory.isError, true, JSON.stringify(inventory));
|
||||
assert.ok(
|
||||
inventory.structuredContent?.result?.tool_availability.some(
|
||||
(tool) => tool.name === "trace_native_values" && tool.available,
|
||||
),
|
||||
"Native value tracing is unavailable in real provider discovery",
|
||||
);
|
||||
const result = await client.callTool(
|
||||
{
|
||||
name: "trace_native_values",
|
||||
arguments: { procedure, max_depth: 2, max_functions: 8, limit: 5000 },
|
||||
},
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
assert.notEqual(result.isError, true, JSON.stringify(result));
|
||||
const mcpEvidence = parseEvidence(result.structuredContent?.evidence);
|
||||
assert.equal(mcpEvidence.operation, "trace_native_values");
|
||||
assert.equal(mcpEvidence.provider.id, "rea-workflow");
|
||||
assert.deepEqual(mcpEvidence.analysis_profile, workflowProfile);
|
||||
const mcpGraph = verifyGraph(
|
||||
result.structuredContent?.result,
|
||||
target,
|
||||
procedure,
|
||||
globalAddress,
|
||||
);
|
||||
assert.deepEqual(mcpEvidence.normalized_result, mcpGraph);
|
||||
assert.deepEqual(mcpGraph, cliGraph);
|
||||
const analyzed = await client.callTool(
|
||||
{ name: "analyze_function", arguments: { procedure } },
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
assert.notEqual(analyzed.isError, true, JSON.stringify(analyzed));
|
||||
const functionEvidence = parseEvidence(
|
||||
analyzed.structuredContent?.evidence,
|
||||
);
|
||||
assert.equal(functionEvidence.operation, "analyze_function");
|
||||
assert.equal(functionEvidence.provider.id, "rea-workflow");
|
||||
const functionProfile = analysisProfileSchema.parse(
|
||||
functionEvidence.analysis_profile,
|
||||
);
|
||||
assert.deepEqual(
|
||||
functionProfile.parameters.upstream_analysis_profile,
|
||||
upstreamProfile,
|
||||
);
|
||||
assert.deepEqual(
|
||||
functionEvidence.normalized_result,
|
||||
analyzed.structuredContent?.result,
|
||||
);
|
||||
const comparison = await client.callTool(
|
||||
{
|
||||
name: "compare_functions",
|
||||
arguments: {
|
||||
left: analyzed.structuredContent.evidence,
|
||||
right: analyzed.structuredContent.evidence,
|
||||
},
|
||||
},
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
assert.notEqual(comparison.isError, true, JSON.stringify(comparison));
|
||||
const comparisonEvidence = parseEvidence(
|
||||
comparison.structuredContent?.evidence,
|
||||
);
|
||||
assert.equal(comparisonEvidence.normalized_result.status, "unchanged");
|
||||
assert.deepEqual(
|
||||
comparisonEvidence.normalized_result,
|
||||
comparison.structuredContent?.result,
|
||||
);
|
||||
assert.ok(
|
||||
comparisonEvidence.evidence_links.includes(functionEvidence.evidence_id),
|
||||
);
|
||||
const bundle = await client.callTool({
|
||||
name: "get_evidence_bundle",
|
||||
arguments: {},
|
||||
});
|
||||
assert.notEqual(bundle.isError, true, JSON.stringify(bundle));
|
||||
for (const evidence of [
|
||||
mcpEvidence,
|
||||
functionEvidence,
|
||||
comparisonEvidence,
|
||||
]) {
|
||||
assert.deepEqual(
|
||||
bundle.structuredContent?.result?.records.find(
|
||||
(record) => record.evidence_id === evidence.evidence_id,
|
||||
),
|
||||
evidence,
|
||||
);
|
||||
}
|
||||
const closed = await client.callTool({
|
||||
name: "close_binary",
|
||||
arguments: {},
|
||||
});
|
||||
assert.notEqual(closed.isError, true, JSON.stringify(closed));
|
||||
const status = await client.callTool({
|
||||
name: "binary_session",
|
||||
arguments: {},
|
||||
});
|
||||
assert.equal(status.structuredContent?.result?.open, false);
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
}
|
||||
return {
|
||||
mocked: false,
|
||||
cli: true,
|
||||
stdio_mcp: true,
|
||||
inline_evidence: true,
|
||||
direct_function_comparison: true,
|
||||
nodes: cliGraph.total_nodes,
|
||||
edges: cliGraph.total_edges,
|
||||
decompilations: cliGraph.decompilations,
|
||||
unknowns: cliGraph.unknowns.length,
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { format } from "prettier";
|
||||
import { formatGeneratedFile } from "./format-generated-file.mjs";
|
||||
|
||||
import { assertSameNames, digest, loadSources } from "./catalog-core.mjs";
|
||||
import {
|
||||
@@ -67,4 +67,7 @@ export const createProductCatalog = async (root) => {
|
||||
|
||||
/** Stable checked-in representation of the product catalog. */
|
||||
export const serializeProductCatalog = (catalog) =>
|
||||
format(JSON.stringify(catalog, null, 2), { parser: "json" });
|
||||
formatGeneratedFile(
|
||||
"docs/product-catalog.json",
|
||||
JSON.stringify(catalog, null, 2),
|
||||
);
|
||||
|
||||
@@ -31,23 +31,21 @@ export const verifyHopperFunctionBasics = async (
|
||||
arguments: {
|
||||
procedure,
|
||||
direction: "outgoing",
|
||||
limit: 10,
|
||||
max_instructions: 100,
|
||||
},
|
||||
},
|
||||
options,
|
||||
),
|
||||
"procedure_references",
|
||||
);
|
||||
if (!Array.isArray(references?.references?.items)) {
|
||||
throw new Error("procedure_references returned an invalid bounded result");
|
||||
if (!Array.isArray(references?.references)) {
|
||||
throw new Error("procedure_references returned an invalid result");
|
||||
}
|
||||
|
||||
const instructions = requireMcpResult(
|
||||
await client.callTool(
|
||||
{
|
||||
name: "read_function_instructions",
|
||||
arguments: { procedure, limit: 2 },
|
||||
arguments: { procedure },
|
||||
},
|
||||
options,
|
||||
),
|
||||
@@ -55,17 +53,14 @@ export const verifyHopperFunctionBasics = async (
|
||||
);
|
||||
if (
|
||||
instructions?.procedure?.address !== procedure ||
|
||||
!Array.isArray(instructions.instructions?.items) ||
|
||||
instructions.instructions.items.length === 0 ||
|
||||
instructions.instructions.items.length > 2
|
||||
!Array.isArray(instructions.instructions) ||
|
||||
instructions.instructions.length === 0
|
||||
) {
|
||||
throw new Error(
|
||||
"read_function_instructions returned an invalid bounded window",
|
||||
);
|
||||
throw new Error("read_function_instructions returned an invalid result");
|
||||
}
|
||||
|
||||
return {
|
||||
outgoingReferenceCount: references.references.items.length,
|
||||
instructionWindowCount: instructions.instructions.items.length,
|
||||
outgoingReferenceCount: references.references.length,
|
||||
instructionWindowCount: instructions.instructions.length,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProvider.js";
|
||||
import {
|
||||
requireMcpResult,
|
||||
requireEvidenceProvider,
|
||||
} from "./mcp-verifier-results.mjs";
|
||||
|
||||
/** Verify real Hopper inventories and address filters through the MCP transport. */
|
||||
export async function verifyHopperInventories(client, options) {
|
||||
const inventoryCounts = {};
|
||||
for (const operation of ["list_strings", "list_names"]) {
|
||||
const called = await client.callTool(
|
||||
{ name: operation, arguments: {} },
|
||||
options,
|
||||
);
|
||||
const inventory = requireMcpResult(called, operation);
|
||||
requireEvidenceProvider(called, operation, HOPPER_PROVIDER_IDENTITY.id);
|
||||
assert.ok(
|
||||
Array.isArray(inventory) && inventory.length > 0,
|
||||
`${operation} omitted the real fixture inventory`,
|
||||
);
|
||||
let previous = -1n;
|
||||
for (const item of inventory) {
|
||||
assert.match(item.address, /^0x[0-9a-f]+$/u);
|
||||
assert.equal(typeof item.value, "string");
|
||||
const address = BigInt(item.address);
|
||||
assert.ok(
|
||||
address > previous,
|
||||
`${operation} is not numerically address-sorted`,
|
||||
);
|
||||
previous = address;
|
||||
}
|
||||
const filtered = requireMcpResult(
|
||||
await client.callTool(
|
||||
{
|
||||
name: operation,
|
||||
arguments: { address: inventory[0].address },
|
||||
},
|
||||
options,
|
||||
),
|
||||
operation,
|
||||
);
|
||||
assert.deepEqual(filtered, [inventory[0]]);
|
||||
inventoryCounts[operation] = inventory.length;
|
||||
}
|
||||
return inventoryCounts;
|
||||
}
|
||||
@@ -39,7 +39,7 @@ const observeVerifierLineage = async (run, observedAt) => {
|
||||
run,
|
||||
observedAt,
|
||||
null,
|
||||
"Windows verifier lineage ownership is unavailable without Job Objects",
|
||||
"The verifier has no retained native Job Object lease for lineage observation; provider jobs are verified by their native owners",
|
||||
);
|
||||
try {
|
||||
const { members, observerPid } = await processSnapshot();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { lstat } from "node:fs/promises";
|
||||
import { promisify } from "node:util";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
|
||||
/** Promisified child_process execution helper. */
|
||||
export const exec = promisify(execFile);
|
||||
@@ -28,9 +29,19 @@ export const runWithStatus = async (command, args, env) => {
|
||||
/** Parse JSON from a command output string. */
|
||||
export const json = (text) => JSON.parse(text);
|
||||
|
||||
/** Verify that the complete advertised MCP catalog matches session metadata. */
|
||||
/** Verify schema validity and that the MCP catalog matches session metadata. */
|
||||
export const verifyCompleteToolCatalog = async (client, options) => {
|
||||
const listed = await client.listTools(undefined, options);
|
||||
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
||||
for (const tool of listed.tools) {
|
||||
for (const kind of ["inputSchema", "outputSchema"]) {
|
||||
const schema = tool[kind];
|
||||
if (schema !== undefined && !ajv.validateSchema(schema))
|
||||
throw new Error(
|
||||
`packaged MCP has invalid ${tool.name}.${kind}: ${ajv.errorsText(ajv.errors)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const status = await client.callTool(
|
||||
{ name: "binary_session", arguments: {} },
|
||||
options,
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ const runtimeFiles = isMcpMode
|
||||
|
||||
if (!(await compiledRuntimeExists(runtimeFiles))) {
|
||||
process.stderr.write(
|
||||
`REA's compiled runtime is missing. Run \`npm ci\` in ${packageRoot} to install dependencies and build REA, then restart it. If this is an installed package, reinstall rea-agents.\n`,
|
||||
`REA's compiled runtime is missing. Run \`npm ci && npm run build:cached\` in ${packageRoot} to install dependencies and build REA, then restart it. If this is an installed package, reinstall rea-agents.\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
} else if (isMcpMode) {
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { stat } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import {
|
||||
developmentTestPlan,
|
||||
parseDevelopmentTestRequest,
|
||||
} from "./lib/development-tests.mjs";
|
||||
|
||||
let child;
|
||||
let forceTimer;
|
||||
let shutdownSignal;
|
||||
const signalExitCodes = { SIGINT: 130, SIGTERM: 143 };
|
||||
for (const signal of Object.keys(signalExitCodes))
|
||||
process.on(signal, () => {
|
||||
if (shutdownSignal !== undefined) return;
|
||||
shutdownSignal = signal;
|
||||
if (child !== undefined && child.exitCode === null) {
|
||||
child.kill(signal);
|
||||
forceTimer = setTimeout(() => child?.kill("SIGKILL"), 1_000);
|
||||
forceTimer.unref?.();
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const [mode, ...arguments_] = process.argv.slice(2);
|
||||
const request = parseDevelopmentTestRequest(mode, arguments_);
|
||||
for (const path of request.paths) {
|
||||
if (!(await stat(path)).isFile())
|
||||
throw new Error(`Test path is not a file: ${path}`);
|
||||
}
|
||||
let baseCommit;
|
||||
if (request.mode === "changed") {
|
||||
const base = await run("git", ["merge-base", "HEAD", request.base], true);
|
||||
if (base.code !== 0)
|
||||
throw new Error(
|
||||
`Cannot resolve test base ${request.base}. Fetch origin/main or pass --base REVISION.\n${base.output.trim()}`,
|
||||
);
|
||||
baseCommit = base.output.trim();
|
||||
}
|
||||
const plan = developmentTestPlan(request, baseCommit);
|
||||
if (request.dryRun) {
|
||||
console.log(JSON.stringify({ ...request, baseCommit, ...plan }, null, 2));
|
||||
} else {
|
||||
console.error(
|
||||
request.mode === "focused"
|
||||
? `Focused tests: ${request.paths.join(", ")}`
|
||||
: `Source-test feedback${baseCommit ? ` from merge base ${baseCommit}` : ""}; compiled boundaries and real providers need explicit verification.`,
|
||||
);
|
||||
if (plan.needsBuild) {
|
||||
if (!process.env.npm_execpath)
|
||||
throw new Error("Run compiled tests through npm run test:focused");
|
||||
const built = await run(process.execPath, [
|
||||
process.env.npm_execpath,
|
||||
"run",
|
||||
"build:cached",
|
||||
]);
|
||||
if (built.code !== 0) process.exitCode = built.code;
|
||||
}
|
||||
if (!process.exitCode && !shutdownSignal) {
|
||||
const tested = await run(process.execPath, [
|
||||
resolve("node_modules/vitest/vitest.mjs"),
|
||||
...plan.vitestArguments,
|
||||
]);
|
||||
process.exitCode = tested.code;
|
||||
}
|
||||
}
|
||||
} catch (cause) {
|
||||
console.error(cause instanceof Error ? cause.message : String(cause));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
if (shutdownSignal) process.exitCode = signalExitCodes[shutdownSignal];
|
||||
|
||||
function run(executable, arguments_, capture = false) {
|
||||
if (shutdownSignal)
|
||||
return Promise.resolve({
|
||||
code: signalExitCodes[shutdownSignal],
|
||||
output: "",
|
||||
});
|
||||
child = spawn(executable, arguments_, {
|
||||
stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
||||
});
|
||||
let output = "";
|
||||
if (capture) {
|
||||
child.stdout.on("data", (data) => (output += data.toString()));
|
||||
child.stderr.on("data", (data) => (output += data.toString()));
|
||||
}
|
||||
return new Promise((resolveResult, reject) => {
|
||||
child.once("error", reject);
|
||||
child.once("close", (code) => {
|
||||
clearTimeout(forceTimer);
|
||||
resolveResult({ code: code ?? 1, output });
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -12,6 +12,7 @@ const signalExitCodes = { SIGINT: 130, SIGTERM: 143 };
|
||||
|
||||
let lockHeld = false;
|
||||
let child;
|
||||
let forceTimer;
|
||||
let shutdownSignal;
|
||||
|
||||
process.on("SIGINT", () => handleSignal("SIGINT"));
|
||||
@@ -96,14 +97,21 @@ function runCommand() {
|
||||
stdio: "inherit",
|
||||
});
|
||||
child.once("error", reject);
|
||||
child.once("close", (code) => resolveResult({ code }));
|
||||
child.once("close", (code) => {
|
||||
clearTimeout(forceTimer);
|
||||
resolveResult({ code });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleSignal(signal) {
|
||||
if (shutdownSignal !== undefined) return;
|
||||
shutdownSignal = signal;
|
||||
if (child !== undefined && child.exitCode === null) child.kill(signal);
|
||||
if (child !== undefined && child.exitCode === null) {
|
||||
child.kill(signal);
|
||||
forceTimer = setTimeout(() => child?.kill("SIGKILL"), 1_000);
|
||||
forceTimer.unref?.();
|
||||
}
|
||||
}
|
||||
|
||||
async function readOwner() {
|
||||
|
||||
@@ -82,7 +82,7 @@ try {
|
||||
"depth = 1",
|
||||
],
|
||||
["static", "inferred"],
|
||||
["runtime", "replay"],
|
||||
["runtime", "behavioral probes"],
|
||||
],
|
||||
prompt: `Without directly reading target files, compare the default export in parser.mjs between ${targets.javascriptShapeLeft} and ${targets.javascriptShapeRight}. Use REA to analyze each shipped artifact and then compare its exact static export return shapes. State the exact heading-shape change, cite produced Evidence, and distinguish static inference from runtime semantics.`,
|
||||
},
|
||||
@@ -283,13 +283,13 @@ async function createTargets(root, includeManaged) {
|
||||
mkdir(javascriptShapeRight, { recursive: true }),
|
||||
]);
|
||||
await Promise.all([
|
||||
cp(
|
||||
join(repositoryRoot, "tests/fixtures/replay/parser.mjs"),
|
||||
writeFile(
|
||||
join(javascriptShapeLeft, "parser.mjs"),
|
||||
'export default function parse() { return { heading: "Title" }; }\n',
|
||||
),
|
||||
cp(
|
||||
join(repositoryRoot, "tests/fixtures/replay/parser-v2.mjs"),
|
||||
writeFile(
|
||||
join(javascriptShapeRight, "parser.mjs"),
|
||||
'export default function parse() { return { heading: { text: "Title", level: 1 } }; }\n',
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -332,20 +332,6 @@ async function createTargets(root, includeManaged) {
|
||||
}
|
||||
|
||||
async function runCodex(prompt) {
|
||||
const mcpEnvironment = [
|
||||
["REA_BROWSER_OBSERVE_ENABLED", "true"],
|
||||
[
|
||||
"REA_BROWSER_CDP_ENDPOINTS_JSON",
|
||||
JSON.stringify(["http://127.0.0.1:9222"]),
|
||||
],
|
||||
[
|
||||
"REA_BROWSER_ALLOWED_ORIGINS_JSON",
|
||||
JSON.stringify(["http://127.0.0.1:3000"]),
|
||||
],
|
||||
];
|
||||
const mcpEnvironmentOverride = `mcp_servers.rea.env={${mcpEnvironment
|
||||
.map(([name, value]) => `${name}=${JSON.stringify(value)}`)
|
||||
.join(",")}}`;
|
||||
const arguments_ = [
|
||||
"exec",
|
||||
"--ignore-user-config",
|
||||
@@ -367,21 +353,12 @@ async function runCodex(prompt) {
|
||||
`mcp_servers.rea.args=${JSON.stringify([join(repositoryRoot, "scripts/rea.mjs"), "mcp"])}`,
|
||||
"-c",
|
||||
`mcp_servers.rea.startup_timeout_sec=${String(MCP_STARTUP_POLICY.codexStartupTimeoutSeconds)}`,
|
||||
"-c",
|
||||
mcpEnvironmentOverride,
|
||||
...(optionalModel === undefined ? [] : ["--model", optionalModel]),
|
||||
prompt,
|
||||
];
|
||||
const child = spawn(codex, arguments_, {
|
||||
cwd: evaluationRoot,
|
||||
env: {
|
||||
...process.env,
|
||||
REA_BROWSER_OBSERVE_ENABLED: "true",
|
||||
REA_BROWSER_CDP_ENDPOINTS_JSON: JSON.stringify(["http://127.0.0.1:9222"]),
|
||||
REA_BROWSER_ALLOWED_ORIGINS_JSON: JSON.stringify([
|
||||
"http://127.0.0.1:3000",
|
||||
]),
|
||||
},
|
||||
env: process.env,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
const events = [];
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
import { inspectAppleDispatchMetadata } from "../dist/native/AppleDispatchMetadata.js";
|
||||
|
||||
if (process.platform !== "darwin")
|
||||
throw new Error(
|
||||
"Apple dispatch verification requires macOS with a host Objective-C compiler and Foundation SDK",
|
||||
);
|
||||
const exec = promisify(execFile);
|
||||
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,
|
||||
});
|
||||
}
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ ok: true, target_executed: false, fixtures: results })}\n`,
|
||||
);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtemp, mkdir, 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 {
|
||||
artifactCli,
|
||||
artifactMcpResult,
|
||||
withArtifactMcp,
|
||||
} from "./lib/artifact-e2e.mjs";
|
||||
|
||||
if (process.platform !== "darwin")
|
||||
throw new Error(
|
||||
"Asset catalog E2E requires macOS, Xcode actool and assetutil",
|
||||
);
|
||||
const exec = promisify(execFile);
|
||||
await exec("/usr/bin/xcrun", ["--find", "actool"]);
|
||||
await exec("/usr/bin/xcrun", ["--find", "clang"]);
|
||||
await exec("/usr/bin/assetutil", ["--version"]);
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-asset-e2e-"));
|
||||
try {
|
||||
const app = join(root, "Fixture.app");
|
||||
const resources = join(app, "Contents", "Resources");
|
||||
await mkdir(resources, { recursive: true });
|
||||
await mkdir(join(app, "Contents", "MacOS"), { recursive: true });
|
||||
await exec("/usr/bin/xcrun", [
|
||||
"clang",
|
||||
fileURLToPath(
|
||||
new URL(
|
||||
"../tests/conformance/interface-builder/fixture.c",
|
||||
import.meta.url,
|
||||
),
|
||||
),
|
||||
"-o",
|
||||
join(app, "Contents", "MacOS", "Fixture"),
|
||||
]);
|
||||
await writeFile(
|
||||
join(app, "Contents", "Info.plist"),
|
||||
'<?xml version="1.0"?><plist version="1.0"><dict><key>CFBundleExecutable</key><string>Fixture</string><key>CFBundleIdentifier</key><string>dev.rea.asset-fixture</string></dict></plist>',
|
||||
);
|
||||
await exec("/usr/bin/xcrun", [
|
||||
"actool",
|
||||
fileURLToPath(
|
||||
new URL("../tests/conformance/native/Assets.xcassets", import.meta.url),
|
||||
),
|
||||
"--compile",
|
||||
resources,
|
||||
"--platform",
|
||||
"macosx",
|
||||
"--minimum-deployment-target",
|
||||
"12.0",
|
||||
]);
|
||||
const raw = JSON.parse(
|
||||
(
|
||||
await exec("/usr/bin/assetutil", [
|
||||
"--info",
|
||||
join(resources, "Assets.car"),
|
||||
])
|
||||
).stdout,
|
||||
);
|
||||
const sha256 = createHash("sha256")
|
||||
.update(await readFile(join(resources, "Assets.car")))
|
||||
.digest("hex");
|
||||
const cli = await artifactCli("inspect-asset-catalog", app);
|
||||
assert.deepEqual(
|
||||
cli.records.map(({ metadata }) => metadata),
|
||||
raw,
|
||||
);
|
||||
assert.equal(cli.catalogs[0].sha256, sha256);
|
||||
assert.ok(
|
||||
cli.records.some(
|
||||
({ asset_name, metadata }) =>
|
||||
asset_name === "FixtureColor" && metadata.AssetType === "Color",
|
||||
),
|
||||
);
|
||||
await withArtifactMcp(app, async (client) => {
|
||||
assert.deepEqual(
|
||||
await artifactMcpResult(client, "inspect_asset_catalog"),
|
||||
cli,
|
||||
);
|
||||
const first = await artifactMcpResult(client, "inspect_asset_catalog", {
|
||||
limit: 1,
|
||||
});
|
||||
assert.equal(first.next_offset, 1);
|
||||
const next = await artifactMcpResult(client, "inspect_asset_catalog", {
|
||||
offset: first.next_offset,
|
||||
});
|
||||
assert.deepEqual([...first.records, ...next.records], cli.records);
|
||||
const invalid = await client.callTool({
|
||||
name: "inspect_asset_catalog",
|
||||
arguments: { offset: -1 },
|
||||
});
|
||||
assert.equal(invalid.isError, true);
|
||||
});
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ ok: true, mocked: false, cli: true, stdio_mcp: true, records: raw.length, pagination: true, malformed_input_rejected: true })}\n`,
|
||||
);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
artifactCli,
|
||||
artifactMcpResult,
|
||||
withArtifactMcp,
|
||||
} from "./lib/artifact-e2e.mjs";
|
||||
|
||||
if (process.platform !== "darwin")
|
||||
throw new Error(
|
||||
"Real Foundation keyed archive verification requires macOS and Xcode Swift tools",
|
||||
);
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-keyed-fixture-"));
|
||||
try {
|
||||
const archive = join(root, "model.plist");
|
||||
await promisify(execFile)("/usr/bin/xcrun", [
|
||||
"swift",
|
||||
"-module-cache-path",
|
||||
join(root, "modules"),
|
||||
fileURLToPath(
|
||||
new URL(
|
||||
"../tests/conformance/native/keyed-archive.swift",
|
||||
import.meta.url,
|
||||
),
|
||||
),
|
||||
archive,
|
||||
]);
|
||||
const graph = await artifactCli("inspect-keyed-archive", archive);
|
||||
const digest = createHash("sha256")
|
||||
.update(await readFile(archive))
|
||||
.digest("hex");
|
||||
assert.equal(graph.archive_sha256, digest);
|
||||
assert.equal(graph.target_sha256, digest);
|
||||
await withArtifactMcp(archive, async (client) => {
|
||||
assert.deepEqual(
|
||||
await artifactMcpResult(client, "inspect_keyed_archive"),
|
||||
graph,
|
||||
);
|
||||
const page = await artifactMcpResult(client, "inspect_keyed_archive", {
|
||||
offset: 1,
|
||||
limit: 1,
|
||||
});
|
||||
assert.deepEqual(page.objects, [graph.objects[1]]);
|
||||
assert.equal(page.next_offset, 2);
|
||||
const invalid = await client.callTool({
|
||||
name: "inspect_keyed_archive",
|
||||
arguments: { path: "../other.plist" },
|
||||
});
|
||||
assert.equal(invalid.isError, true);
|
||||
});
|
||||
const xml = join(root, "model.xml.plist");
|
||||
await promisify(execFile)("/usr/bin/plutil", [
|
||||
"-convert",
|
||||
"xml1",
|
||||
"-o",
|
||||
xml,
|
||||
archive,
|
||||
]);
|
||||
const xmlGraph = await artifactCli("inspect-keyed-archive", xml);
|
||||
const expected = JSON.parse(
|
||||
await readFile(
|
||||
new URL(
|
||||
"../tests/fixtures/golden/keyed-archive/graph.json",
|
||||
import.meta.url,
|
||||
),
|
||||
"utf8",
|
||||
),
|
||||
);
|
||||
assert.deepEqual(
|
||||
Object.fromEntries(
|
||||
Object.entries(xmlGraph).filter(
|
||||
([key]) =>
|
||||
!["archive_path", "archive_sha256", "target_sha256"].includes(key),
|
||||
),
|
||||
),
|
||||
expected,
|
||||
);
|
||||
assert.deepEqual(
|
||||
graph.references.map(({ source, path, target, status }) => ({
|
||||
source,
|
||||
path,
|
||||
target,
|
||||
status,
|
||||
})),
|
||||
xmlGraph.references.map(({ source, path, target, status }) => ({
|
||||
source,
|
||||
path,
|
||||
target,
|
||||
status,
|
||||
})),
|
||||
);
|
||||
if (
|
||||
graph?.archive_format !== "binary-plist" ||
|
||||
!graph.references.some(
|
||||
(link) =>
|
||||
link.source !== null &&
|
||||
link.source === link.target &&
|
||||
link.status === "resolved",
|
||||
) ||
|
||||
!graph.objects.some((item) => item.class_name?.includes("ReaArchiveRecord"))
|
||||
)
|
||||
throw new Error(
|
||||
`Real Foundation archive graph drifted: ${JSON.stringify(graph)}`,
|
||||
);
|
||||
const record = graph.objects.find((item) =>
|
||||
item.class_name?.includes("ReaArchiveRecord"),
|
||||
);
|
||||
if (
|
||||
graph.references.filter(
|
||||
(link) => link.target === record.id && link.source !== record.id,
|
||||
).length < 2
|
||||
)
|
||||
throw new Error("Real shared object identity was not preserved");
|
||||
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`,
|
||||
);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
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";
|
||||
|
||||
// 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.
|
||||
const leaf = "x".repeat(1024 * 1024);
|
||||
const encodedLeaf = JSON.stringify(leaf);
|
||||
const count = Math.ceil(constants.MAX_STRING_LENGTH / encodedLeaf.length) + 1;
|
||||
const value = { payload: Array.from({ length: count }, () => leaf) };
|
||||
const expected = createHash("sha256");
|
||||
expected.update('{"payload":[');
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
if (index > 0) expected.update(",");
|
||||
expected.update(encodedLeaf);
|
||||
}
|
||||
expected.update("]}");
|
||||
assert.equal(canonicalDigest(value), expected.digest("hex"));
|
||||
|
||||
const evidence = createEvidence(
|
||||
undefined,
|
||||
{ id: "large-json-fixture", name: "Large JSON fixture", version: "1" },
|
||||
{ operation: "inspect_fixture", parameters: {}, result: value },
|
||||
);
|
||||
assert.equal(parseEvidence(evidence).evidence_id, evidence.evidence_id);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
verified: true,
|
||||
serialized_value_bytes: count * encodedLeaf.length + count - 1 + 14,
|
||||
engine_string_limit: constants.MAX_STRING_LENGTH,
|
||||
evidence_id: evidence.evidence_id,
|
||||
})}\n`,
|
||||
);
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import assert from "node:assert/strict";
|
||||
@@ -9,10 +9,8 @@ import { inspectManagedNativeBoundariesBytes } from "../dist/dotnet/ManagedNativ
|
||||
import { compareManagedMemberPaths } from "../dist/application/ManagedMemberComparisonService.js";
|
||||
import { verifyManagedNativeBoundariesEvidence } from "../dist/application/ManagedNativeVerificationService.js";
|
||||
import { importManagedReconstructionEvidence } from "../dist/application/ManagedReconstructionService.js";
|
||||
import { planManagedRuntimeCorrelationEvidence } from "../dist/application/ManagedRuntimeCorrelationService.js";
|
||||
import { projectManagedApplicationGraphEvidence } from "../dist/application/ManagedApplicationGraphService.js";
|
||||
import { traceApplicationFeatureEvidence } from "../dist/application/JavaScriptApplicationWorkflowService.js";
|
||||
import { createPermissionAuthority } from "../dist/application/PermissionAuthority.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../dist/application/InvestigationProviders.js";
|
||||
import { createEvidence } from "../dist/domain/evidence.js";
|
||||
import {
|
||||
@@ -250,31 +248,8 @@ try {
|
||||
assert.equal(obfuscatedMembers.types[0]?.full_name, "Fixture.ꙮType");
|
||||
assert.equal(obfuscatedMembers.methods[0]?.name, "λ⛧");
|
||||
assert.equal(obfuscatedMembers.fields[0]?.name, "字段");
|
||||
const runtimeExecutable = join(workspace, "dotnet");
|
||||
await writeFile(runtimeExecutable, "#!/bin/sh\n");
|
||||
const runtimeCeiling = {
|
||||
capability: "managed_runtime",
|
||||
roots: [workspace],
|
||||
executables: [runtimeExecutable],
|
||||
environment_names: [],
|
||||
network: "none",
|
||||
mount: false,
|
||||
};
|
||||
const runtimeAuthority = await createPermissionAuthority(
|
||||
[runtimeCeiling],
|
||||
[
|
||||
{
|
||||
...runtimeCeiling,
|
||||
grant_id: "administrator:managed_runtime",
|
||||
lifetime: "administrator",
|
||||
operation_identity: null,
|
||||
expires_at: null,
|
||||
},
|
||||
],
|
||||
);
|
||||
assert.equal(runtimeAuthority.ok, true);
|
||||
const runtimeMethod = obfuscatedMembers.methods[0];
|
||||
assert.ok(runtimeMethod);
|
||||
const reconstructionMethod = obfuscatedMembers.methods[0];
|
||||
assert.ok(reconstructionMethod);
|
||||
const obfuscatedMembersEvidence = createEvidence(
|
||||
undefined,
|
||||
MANAGED_STATIC_PROVIDER,
|
||||
@@ -297,9 +272,9 @@ try {
|
||||
},
|
||||
methods: [
|
||||
{
|
||||
token: runtimeMethod.token,
|
||||
signature_sha256: runtimeMethod.signature.raw_sha256,
|
||||
normalized_il_sha256: runtimeMethod.body.normalized_il_sha256,
|
||||
token: reconstructionMethod.token,
|
||||
signature_sha256: reconstructionMethod.signature.raw_sha256,
|
||||
normalized_il_sha256: reconstructionMethod.body.normalized_il_sha256,
|
||||
reconstruction: {
|
||||
kind: "decompiled-csharp",
|
||||
language: "csharp",
|
||||
@@ -316,48 +291,6 @@ try {
|
||||
false,
|
||||
);
|
||||
assert.equal(reconstructionImport.value.confidence, "inferred");
|
||||
const runtimePlan = await planManagedRuntimeCorrelationEvidence(
|
||||
{
|
||||
policy: () => ({
|
||||
status: "enabled",
|
||||
roots: [workspace],
|
||||
executablePath: runtimeExecutable,
|
||||
}),
|
||||
authority: runtimeAuthority.value,
|
||||
},
|
||||
{
|
||||
static_members: obfuscatedMembersEvidence,
|
||||
method: {
|
||||
token: runtimeMethod.token,
|
||||
signature_sha256: runtimeMethod.signature.raw_sha256,
|
||||
normalized_il_sha256: runtimeMethod.body.normalized_il_sha256,
|
||||
},
|
||||
requested_effect: "debugger",
|
||||
host: {
|
||||
os: "linux",
|
||||
clr_family: "dotnet",
|
||||
architecture: "x86_64",
|
||||
},
|
||||
bounds: {
|
||||
timeout_ms: 5_000,
|
||||
max_threads: 32,
|
||||
max_output_bytes: 65_536,
|
||||
allow_network: false,
|
||||
allow_ui: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
assert.equal(runtimePlan.ok, true);
|
||||
assert.equal(runtimePlan.value.normalized_result.executed, false);
|
||||
assert.equal(
|
||||
runtimePlan.value.normalized_result.authority_model.capability,
|
||||
"managed_runtime",
|
||||
);
|
||||
assert.equal(
|
||||
runtimePlan.value.normalized_result.effect_taxonomy.uses_debugger,
|
||||
true,
|
||||
);
|
||||
|
||||
const left = await fixture("token-drift-left.exe", {
|
||||
methodName: "StableSemanticSlice",
|
||||
});
|
||||
@@ -379,7 +312,10 @@ try {
|
||||
});
|
||||
assert.equal(comparison.ok, true);
|
||||
const comparisonResult = comparison.value.normalized_result;
|
||||
assert.equal(comparisonResult.algorithm.name_matching, "not-used");
|
||||
assert.equal(
|
||||
comparisonResult.algorithm.name_matching,
|
||||
"exact-signature-fallback",
|
||||
);
|
||||
assert.equal(comparisonResult.matching.exact_il_signature, 1);
|
||||
assert.equal(
|
||||
comparisonResult.methods.some(
|
||||
@@ -391,6 +327,30 @@ try {
|
||||
true,
|
||||
);
|
||||
|
||||
const unavailableBodyLeft = await fixture("partial-body-left.exe", {
|
||||
methods: [{ name: "PartialBodyTarget", body: Buffer.from([0xff]) }],
|
||||
});
|
||||
const unavailableBodyRight = await fixture("partial-body-right.exe", {
|
||||
methods: [{ name: "PartialBodyTarget", body: defaultIlBody }],
|
||||
});
|
||||
const partialBodyComparison = await compareManagedMemberPaths({
|
||||
leftPath: unavailableBodyLeft.path,
|
||||
rightPath: unavailableBodyRight.path,
|
||||
memberLimits: {
|
||||
maxFileBytes: 1024 * 1024,
|
||||
...memberLimits,
|
||||
},
|
||||
comparisonLimits,
|
||||
});
|
||||
assert.equal(partialBodyComparison.ok, true);
|
||||
const partialBodyMethod =
|
||||
partialBodyComparison.value.normalized_result.methods.find(
|
||||
({ left }) => left?.name === "PartialBodyTarget",
|
||||
);
|
||||
assert.equal(partialBodyMethod?.status, "unknown");
|
||||
assert.equal(partialBodyMethod?.match.basis, "exact-signature");
|
||||
assert.equal(partialBodyMethod?.dimensions.includes("body-coverage"), true);
|
||||
|
||||
const nativeOnly = await fixtureBytes(
|
||||
"native-only.exe",
|
||||
buildNativePeFixture(),
|
||||
@@ -436,7 +396,7 @@ try {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
verified:
|
||||
12 +
|
||||
11 +
|
||||
(operatorManifest === null ? 0 : 1) +
|
||||
(ilspyOracle === null ? 0 : 1),
|
||||
managedSurfaces: [
|
||||
@@ -446,7 +406,6 @@ try {
|
||||
"compare_managed_members",
|
||||
"verify_managed_native_boundaries",
|
||||
"import_managed_reconstruction",
|
||||
"plan_managed_runtime_correlation",
|
||||
"project_managed_application_graph",
|
||||
],
|
||||
coverage: [
|
||||
@@ -457,7 +416,6 @@ try {
|
||||
"managed-application-graph-projection",
|
||||
"unicode-obfuscated-identifiers",
|
||||
"decompiler-reconstruction-import",
|
||||
"runtime-correlation-admission-plan",
|
||||
"mvid-and-token-drift",
|
||||
"not-managed",
|
||||
"malformed-metadata",
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import { mkdtemp, mkdir, writeFile, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
import { observeNativeUi } from "../dist/native/NativeUiObservation.js";
|
||||
|
||||
if (process.platform !== "darwin")
|
||||
throw new Error(
|
||||
"Native UI verification requires macOS, an interactive desktop and Xcode command-line tools",
|
||||
);
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-ui-fixture-"));
|
||||
let child;
|
||||
let fixturePid;
|
||||
try {
|
||||
const contents = join(root, "Fixture.app", "Contents");
|
||||
await mkdir(join(contents, "MacOS"), { recursive: true });
|
||||
await writeFile(
|
||||
join(contents, "Info.plist"),
|
||||
`<?xml version="1.0"?><plist version="1.0"><dict><key>CFBundleExecutable</key><string>fixture</string><key>CFBundleIdentifier</key><string>io.rea.source-owned-ui-fixture</string><key>CFBundleName</key><string>REA UI Fixture</string><key>CFBundlePackageType</key><string>APPL</string></dict></plist>`,
|
||||
);
|
||||
const executable = join(contents, "MacOS", "fixture");
|
||||
await promisify(execFile)("/usr/bin/xcrun", [
|
||||
"swiftc",
|
||||
"-module-cache-path",
|
||||
join(root, "modules"),
|
||||
fileURLToPath(
|
||||
new URL("../tests/conformance/native/ui.swift", import.meta.url),
|
||||
),
|
||||
"-o",
|
||||
executable,
|
||||
]);
|
||||
const coordinates = join(root, "scope.json");
|
||||
await writeFile(coordinates, "");
|
||||
child = spawn(
|
||||
"/usr/bin/open",
|
||||
["-W", "-n", "-g", "--stdout", coordinates, join(root, "Fixture.app")],
|
||||
{ stdio: "ignore" },
|
||||
);
|
||||
let scope;
|
||||
const deadline = Date.now() + 20000;
|
||||
while (Date.now() < deadline) {
|
||||
const output = await readFile(coordinates, "utf8");
|
||||
if (output.includes("\n")) {
|
||||
scope = JSON.parse(output.split("\n")[0]);
|
||||
break;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
if (scope === undefined)
|
||||
throw new Error(
|
||||
"Source-owned UI fixture failed to expose one window within 20 seconds",
|
||||
);
|
||||
fixturePid = scope.pid;
|
||||
const target = await parseBinaryTarget(executable);
|
||||
if (!target.ok) throw target.error;
|
||||
const observation = await observeNativeUi(target.value, "observe_native_ui", {
|
||||
...scope,
|
||||
});
|
||||
if (
|
||||
!observation.ok &&
|
||||
!/accessibility-denied|screen-recording-denied/u.test(
|
||||
observation.error.message,
|
||||
)
|
||||
)
|
||||
throw observation.error;
|
||||
if (!observation.ok && !process.argv.includes("--allow-permission-denial"))
|
||||
throw new Error(
|
||||
`Native UI E2E requires successful capture; permissions prevented coverage: ${observation.error.message}. Use --allow-permission-denial only for the separate permission diagnostic lane.`,
|
||||
);
|
||||
let scenarioStatus = "not-run-permission-denied";
|
||||
if (observation.ok) {
|
||||
const button = observation.value.initial.nodes.find(
|
||||
(node) => node.title === "Increment REA fixture",
|
||||
);
|
||||
if (button === undefined)
|
||||
throw new Error("Source-owned fixture button is absent from AX capture");
|
||||
const scenario = await observeNativeUi(
|
||||
target.value,
|
||||
"capture_native_ui_scenario",
|
||||
{
|
||||
...scope,
|
||||
restore: "leave-as-is",
|
||||
screenshot: false,
|
||||
steps: [
|
||||
{ kind: "click", path: button.path },
|
||||
{ kind: "wait", milliseconds: 200 },
|
||||
],
|
||||
},
|
||||
);
|
||||
if (!scenario.ok) throw scenario.error;
|
||||
if (
|
||||
scenario.value.steps.some((step) => step.outcome !== "completed") ||
|
||||
!scenario.value.steps
|
||||
.at(-1)
|
||||
?.after?.nodes.some((node) => node.title === "REA fixture incremented")
|
||||
)
|
||||
throw new Error(
|
||||
`Source-owned AX scenario failed: ${JSON.stringify(scenario.value.steps.map(({ outcome, reason }) => ({ outcome, reason })))}`,
|
||||
);
|
||||
scenarioStatus = "selected-AX-button-action-observed";
|
||||
const cli = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[
|
||||
fileURLToPath(new URL("./rea.mjs", import.meta.url)),
|
||||
"capture-native-ui-scenario",
|
||||
executable,
|
||||
"--pid",
|
||||
String(scope.pid),
|
||||
"--window-id",
|
||||
String(scope.window_id),
|
||||
"--restore",
|
||||
"leave-as-is",
|
||||
"--steps",
|
||||
JSON.stringify([
|
||||
{ kind: "click", path: button.path },
|
||||
{ kind: "wait", milliseconds: 200 },
|
||||
]),
|
||||
"--json",
|
||||
],
|
||||
{ maxBuffer: 32 * 1024 * 1024, timeout: 180000 },
|
||||
);
|
||||
const evidence = JSON.parse(cli.stdout);
|
||||
if (
|
||||
evidence.normalized_result?.steps?.some(
|
||||
(step) => step.outcome !== "completed",
|
||||
) ||
|
||||
evidence.normalized_result?.steps?.length !== 2
|
||||
)
|
||||
throw new Error(
|
||||
`Native UI CLI did not preserve the selected scenario: ${JSON.stringify(evidence.error ?? evidence.normalized_result?.steps?.map(({ outcome, reason }) => ({ outcome, reason })))}`,
|
||||
);
|
||||
scenarioStatus = "selected-AX-button-action-and-CLI-parity-observed";
|
||||
}
|
||||
const mismatch = await observeNativeUi(
|
||||
{ ...target.value, sha256: "0".repeat(64) },
|
||||
"observe_native_ui",
|
||||
scope,
|
||||
);
|
||||
if (mismatch.ok || !mismatch.error.message.includes("target-mismatch"))
|
||||
throw new Error("Real helper did not reject changed target bytes");
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ ok: true, verification_status: observation.ok ? "passed" : "permission-boundary-only", positive_e2e: observation.ok, observation: observation.ok ? "captured-selected-fixture-window" : "os-permission-denial-verified", scenario: scenarioStatus, permission_failure: observation.ok ? null : observation.error.message, mismatch_rejected: true, target_owned: true })}\n`,
|
||||
);
|
||||
} finally {
|
||||
if (fixturePid !== undefined) {
|
||||
try {
|
||||
process.kill(fixturePid, "SIGTERM");
|
||||
} catch (error) {
|
||||
if (error.code !== "ESRCH") throw error;
|
||||
}
|
||||
}
|
||||
if (child !== undefined && child.exitCode === null) {
|
||||
const exited = new Promise((resolve) => child.once("exit", resolve));
|
||||
child.kill("SIGTERM");
|
||||
await exited;
|
||||
}
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/** Derive agent and Hopper setup support from the corresponding doctor checks. */
|
||||
export function packageSetupSupport(
|
||||
checks:
|
||||
| readonly { readonly name: string; readonly ok: boolean }[]
|
||||
| undefined,
|
||||
platform: NodeJS.Platform,
|
||||
): {
|
||||
supportedSetupHost: boolean;
|
||||
hopperSetupSupported: boolean;
|
||||
};
|
||||
|
||||
/** Validate --help, --llms, and doctor output and determine host setup support. */
|
||||
export function verifyPackageDiscovery(input: {
|
||||
readonly cli: string;
|
||||
readonly environment: Readonly<Record<string, string | undefined>>;
|
||||
}): Promise<{
|
||||
supportedSetupHost: boolean;
|
||||
hopperSetupSupported: boolean;
|
||||
}>;
|
||||
@@ -2,7 +2,7 @@ import { json, run, runWithStatus } from "./lib/verify-package-core.mjs";
|
||||
|
||||
const REQUIRED_HELP_COMMANDS = [
|
||||
"setup",
|
||||
"upgrade",
|
||||
"update",
|
||||
"inspect-artifact",
|
||||
"extract-artifact",
|
||||
"import-reference-source",
|
||||
@@ -27,6 +27,17 @@ const REQUIRED_LLM_TOPICS = [
|
||||
"analyze-javascript-application",
|
||||
];
|
||||
|
||||
/** Derive agent and Hopper setup support from the corresponding doctor checks. */
|
||||
export function packageSetupSupport(checks, platform) {
|
||||
const supportedSetupHost =
|
||||
checks?.find(({ name }) => name === "node")?.ok === true;
|
||||
const hopperSetupSupported =
|
||||
supportedSetupHost &&
|
||||
checks?.find(({ name }) => name === "host")?.ok === true &&
|
||||
platform !== "win32";
|
||||
return { supportedSetupHost, hopperSetupSupported };
|
||||
}
|
||||
|
||||
/** Validate --help, --llms, and doctor output and determine host setup support. */
|
||||
export async function verifyPackageDiscovery({ cli, environment }) {
|
||||
const help = await run(cli, ["--help"], environment);
|
||||
@@ -37,8 +48,11 @@ export async function verifyPackageDiscovery({ cli, environment }) {
|
||||
environment,
|
||||
);
|
||||
const doctor = json(doctorExecution.stdout);
|
||||
const supportedSetupHost =
|
||||
doctor.checks?.find(({ name }) => name === "host")?.ok === true;
|
||||
const { supportedSetupHost, hopperSetupSupported } = packageSetupSupport(
|
||||
doctor.checks,
|
||||
process.platform,
|
||||
);
|
||||
const hopperReady = doctor.checks?.find(({ name }) => name === "hopper")?.ok;
|
||||
const expectedDoctorHealth = doctor.checks?.every(({ ok }) => ok) === true;
|
||||
const missingHelp = REQUIRED_HELP_COMMANDS.filter(
|
||||
(command) => !help.includes(command),
|
||||
@@ -51,10 +65,10 @@ export async function verifyPackageDiscovery({ cli, environment }) {
|
||||
missingLlms.length !== 0 ||
|
||||
doctor.healthy !== expectedDoctorHealth ||
|
||||
doctorExecution.status !== (expectedDoctorHealth ? 0 : 1) ||
|
||||
doctor.checks?.find(({ name }) => name === "hopper")?.ok !== true
|
||||
(hopperSetupSupported && hopperReady !== true)
|
||||
)
|
||||
throw new Error(
|
||||
`packaged CLI discovery or doctor failed: ${JSON.stringify({ helpSetup: help.includes("setup"), llmsDecompile: llms.includes("decompile"), doctor })}`,
|
||||
);
|
||||
return { supportedSetupHost };
|
||||
return { supportedSetupHost, hopperSetupSupported };
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user