Compare commits
72
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -281,10 +281,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
|
||||
@@ -335,6 +339,8 @@ jobs:
|
||||
run: >-
|
||||
npx vitest run --coverage.enabled=false
|
||||
tests/boundary/filesystem/binaryTarget.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
|
||||
@@ -345,6 +351,8 @@ jobs:
|
||||
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:package:windows
|
||||
- uses: actions/upload-artifact@v4
|
||||
@@ -366,6 +374,7 @@ jobs:
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run docs:check
|
||||
- run: npm run docs:api:cached
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: api-docs
|
||||
|
||||
@@ -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"
|
||||
+4
-1
@@ -7,7 +7,7 @@ __pycache__/
|
||||
dist/
|
||||
build/
|
||||
docs/api/
|
||||
node_modules/
|
||||
node_modules
|
||||
*.egg-info/
|
||||
*.egg
|
||||
|
||||
@@ -42,3 +42,6 @@ test-results.xml
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Local reference sources and analysis artifacts
|
||||
/_reference/
|
||||
|
||||
@@ -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/**"
|
||||
]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
dist
|
||||
docs/api
|
||||
node_modules
|
||||
CHANGELOG.md
|
||||
package-lock.json
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "3.2.1"
|
||||
".": "4.0.0"
|
||||
}
|
||||
|
||||
@@ -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:api:cached` separately renders API HTML when needed.
|
||||
- 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,7 +87,7 @@ 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.
|
||||
|
||||
|
||||
+127
@@ -1,5 +1,132 @@
|
||||
# Changelog
|
||||
|
||||
## [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
-28
@@ -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
|
||||
@@ -22,36 +22,45 @@ worktrees. After a package, lockfile, or managed-skill version change, run
|
||||
|
||||
Keep dependencies flowing inward through the existing domain, contracts, provider, application, server, and adapter layers. Parse unknown values at process and protocol boundaries, model expected failures with `Result`, and preserve the canonical tool inventory defined by `TOOL_CONTRACTS` unless a deliberate contract change updates every verifier, generated catalog artifact, and snapshot. Prefer capability- and session-scoped tool advertisement over schema truncation.
|
||||
|
||||
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 without rendering API HTML.
|
||||
`docs:generate` regenerates those files; render API HTML separately with
|
||||
`npm run docs:api:cached`. PR CI renders and uploads the `api-docs` artifact.
|
||||
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 +101,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`, `npm run docs:api:cached`, 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 +127,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,52 @@
|
||||
[](#tool-catalog-for-investigation)
|
||||
[](https://nodejs.org/)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
[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 and macOS, 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 +87,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, 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)
|
||||
- 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-read-only-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 and currently unavailable. The required Windows process ownership, private-directory permissions, and safe-path checks are not implemented. See [Windows Ghidra P0](docs/windows-ghidra-p0.md) for the remaining requirements.
|
||||
|
||||
If something is not working, run:
|
||||
|
||||
@@ -199,13 +157,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 +178,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
|
||||
|
||||
The Ghidra adapter supports the exact official Ghidra 12.1.4 release with a 64-bit full JDK 21 on Linux x64 and macOS x64/arm64. macOS additionally requires the matching native Ghidra decompiler; the adapter does not build or install it. 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 +196,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 **22 read-only operations** for functions, strings, symbols, assembly, decompilation, calls, references, instructions, and data types. These also support REA's overview, search, call-graph, and function-analysis workflows. Ghidra does not provide GUI controls or annotation changes 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 and macOS use 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 22 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`, `analyze_function`, `inspect_native_instruction`, `resolve_native_call_targets`, and `inspect_native_data_type`. The exact-object primitives report instruction tokens, static call targets, and recovered database layouts; unsupported facets remain explicit. `trace_native_values` composes bounded decompiler-derived argument/parameter and return/output dependencies across resolved calls. `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 host-native fixtures (Linux x64 ELF or macOS Mach-O) and a native DWARF 4 type-layout object. 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 the Windows P0 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 operations remain unavailable pending the controls described in 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 +217,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 +233,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 +245,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 +261,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 +286,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,157 +299,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 approved native desktop observation.
|
||||
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 | 39 | procedures, pseudocode, assembly, strings, names, segments, callers, callees, xrefs, annotations, bounded byte reads, file-offset translation |
|
||||
| Investigation workflows | 15 | `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 | 7 | Mach-O metadata, code signatures, plists, architectures, Swift demangling; Hopper-free and provenance-bearing |
|
||||
| Artifact graph | 5 | complete inline inspection of directories and supported packages, compiled Interface Builder UI graph decoding, paginated Apple asset catalog metadata, 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 | 39 | 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 operations are currently unavailable. 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.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -517,7 +422,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.0", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -526,11 +431,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
|
||||
@@ -586,7 +491,7 @@ Or install the `rea` command globally:
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea --help
|
||||
rea upgrade
|
||||
rea update
|
||||
rea mcp
|
||||
```
|
||||
|
||||
@@ -594,8 +499,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
|
||||
@@ -612,47 +516,27 @@ 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 exposes 22 read-only operations on supported Linux and macOS hosts. GUI controls and annotation changes require Hopper. Windows Ghidra operations remain unavailable.
|
||||
|
||||
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`
|
||||
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
|
||||
@@ -665,28 +549,20 @@ rea inventory-artifact ./app.asar --json | jq . > inventory.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:
|
||||
|
||||
@@ -697,9 +573,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,
|
||||
@@ -716,7 +592,11 @@ verified or absent.
|
||||
|
||||
## Security model
|
||||
|
||||
REA does not provide a hosted analysis service. Hopper and POSIX 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 operations are blocked until REA implements the required process ownership, private-directory permissions, and safe-path checks. Report vulnerabilities through the private process in [SECURITY.md](SECURITY.md).
|
||||
|
||||
## FAQ
|
||||
|
||||
@@ -730,7 +610,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>
|
||||
|
||||
@@ -744,7 +624,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-read-only-analysis-provider).
|
||||
|
||||
</details>
|
||||
|
||||
@@ -765,48 +645,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. PR CI publishes generated API documentation as the `api-docs` artifact.
|
||||
|
||||
`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
|
||||
|
||||
|
||||
+124
-85
@@ -4,7 +4,7 @@
|
||||
|
||||
# REA: هندسة أي شيء عكسيًا
|
||||
|
||||
### واجهة CLI وخادم MCP واحدان يمكّنان وكلاء البرمجة من إجراء هندسة عكسية لأي شيء
|
||||
### استقصِ سلوك التطبيقات والملفات التنفيذية الأصلية باستخدام وكيلك.
|
||||
|
||||
**اعثر على ميزة تعجبك. افهم آلية عملها. ابنها بالطريقة التي تريدها.**
|
||||
|
||||
@@ -13,109 +13,150 @@
|
||||
[](#منصة-أدوات-التحقيق)
|
||||
[](https://nodejs.org/)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
[البدء السريع](#البدء-السريع) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [دليل الأدوات](#منصة-أدوات-التحقيق) · [كيف يعمل](#كيف-يعمل) · [الأسئلة الشائعة](#الأسئلة-الشائعة)
|
||||
[البدء السريع](#البدء-السريع) · [حالة الدعم](#حالة-الدعم) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [منصة أدوات التحقيق](#منصة-أدوات-التحقيق) · [خطة العمل](#خطة-العمل) · [كيف يعمل؟](#كيف-يعمل)
|
||||
|
||||
<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 و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.19+ أو 24.11+ و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 +200,26 @@ rea uninstall --purge-data
|
||||
|
||||
## منصة أدوات التحقيق
|
||||
|
||||
| عائلة الأدوات | العدد | أمثلة |
|
||||
| --------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| فحص الملفات التنفيذية | 39 | الدوال، والشيفرة شبه المصدرية، والتعليمات، والسلاسل، والأسماء، والمراجع، والتعليقات |
|
||||
| التحليل المركب | 15 | النظرة العامة، و`inspect_native_api`، وفك الترجمة الدفعي، ومخططات الاستدعاء، والمراجع، واكتشاف الأنواع |
|
||||
| أدوات macOS الأصلية | 7 | بيانات Mach-O والتوقيعات وملفات plist والمعماريات وفك رموز Swift دون تشغيل Hopper |
|
||||
| رسم القطع الأثرية | 5 | فحص الدلائل والحزم، ورسم واجهة Interface Builder، وبيانات كتالوج أصول Apple، والاستخراج المحدد صراحة |
|
||||
| Managed PE/CLI | 8 | هوية PE/CLI، أعضاء البيانات الوصفية، بصمات CIL، تصريحات P/Invoke/الحدود الأصلية والتحقق منها، إسقاط رسم التطبيق، استيراد إعادة البناء من فك الترجمة، إعادة ربط الرموز، خطط ترابط وقت التشغيل ومقارنة الإصدارات |
|
||||
| مراقبة المتصفح | 9 | التقاط CDP محدود الأصل وتحليل الحزم وخرائط المصدر واكتشاف WebMCP والفروق والأدلة المرئية |
|
||||
| تحليل Electron | 5 | مراقبة سلبية ضمن جذور canonical، ورسم ثابت ومحدود للتطبيق، ومطابقة ثابتة/وقت تشغيل قائمة على Evidence |
|
||||
| وقت تشغيل JavaScript | 2 | اكتشاف أهداف Node/Electron Inspector المعتمدة للاتصال فقط، ومراقبة محدودة للبرامج النصية وسياقات التنفيذ دون تقييم أو تجهيز |
|
||||
| سير عمل التطبيقات | 10 | تتبع محدود ومقارنة إصدارات فريدة ومقارنة ثابتة لأشكال قيم إرجاع الصادرات وإعادة تشغيل وحدات معزولة ومعتمدة على Linux وتوصيف وقت التشغيل المُدار وإغلاق تغطية إعادة البناء ودفاتر التزامات حتمية |
|
||||
| جلسة الملف التنفيذي | 22 | فتح الأهداف وحزم الأدلة وتشغيل آلة إعادة محدودة ومقارنة العمليات والقطع والدوال وسجل المجهولات المتبقية |
|
||||
| عائلة الأدوات | العدد | الاستخدام |
|
||||
| --------------------- | ----: | -------------------------------------------------------------------------------------- |
|
||||
| فحص الملفات التنفيذية | 39 | الدوال والشيفرة شبه المصدرية والتعليمات والسلاسل والرموز والمراجع والتعليقات |
|
||||
| التحليل المركب | 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 وVS Code. تكون تسجيلات REA الموجودة محددة افتراضياً، ويجب اختيار العملاء المكتشفين الآخرين يدوياً. يمكن لأي وكيل يدعم خوادم MCP المحلية الاتصال باستخدام الإعداد التالي:
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
@@ -183,7 +228,7 @@ rea uninstall --purge-data
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@3.2.1", "mcp"]
|
||||
"args": ["-y", "rea-agents@4.0.0", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,7 +249,7 @@ flowchart LR
|
||||
Ghidra --> App
|
||||
```
|
||||
|
||||
يستخدم CLI وخادم MCP محرك التحليل نفسه. تغلق أوامر الطرفية التطبيق عند انتهائها، بينما تبقيه جلسة الوكيل مفتوحًا أثناء الاستقصاء.
|
||||
يستخدم CLI وخادم MCP سير عمل التحليل نفسه. تحرر أوامر الطرفية جلسة الجسر الخاصة بها عند انتهائها، بينما يمكن لجلسة الوكيل الحفاظ على الاتصال أثناء الاستقصاء. إغلاق جلسة REA لا يغلق تطبيق Hopper الذي يستخدمه المستخدم.
|
||||
|
||||
## أوامر CLI
|
||||
|
||||
@@ -221,15 +266,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 +297,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>
|
||||
|
||||
|
||||
+119
-75
@@ -4,7 +4,7 @@
|
||||
|
||||
# REA:あらゆるものをリバースエンジニアリング
|
||||
|
||||
### コーディングエージェントがあらゆるものをリバースエンジニアリングするための、ひとつの CLI / MCP サーバー
|
||||
### エージェントでアプリの動作からネイティブバイナリまで調べる。
|
||||
|
||||
**気になる機能を見つけ、仕組みを理解し、自分の形で実装する。**
|
||||
|
||||
@@ -13,35 +13,49 @@
|
||||
[](#調査ツールカタログ)
|
||||
[](https://nodejs.org/)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
[クイックスタート](#クイックスタート) · [バイナリから動作へ](#バイナリから動作へ) · [ツールカタログ](#調査ツールカタログ) · [仕組み](#仕組み) · [FAQ](#faq)
|
||||
[クイックスタート](#クイックスタート) · [現在の対応状況](#現在の対応状況) · [バイナリから動作へ](#バイナリから動作へ) · [調査ツールカタログ](#調査ツールカタログ) · [ロードマップ](#ロードマップ) · [仕組み](#仕組み)
|
||||
|
||||
<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 +67,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、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.19+ または 24.11+ と 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 +189,26 @@ REA は手順 1〜5 のバイナリ解析を処理し、手順 6 はエージェ
|
||||
|
||||
## 調査ツールカタログ
|
||||
|
||||
| ツール群 | 数 | 例 |
|
||||
| ---------------------- | --: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| バイナリ調査 | 39 | プロシージャ、疑似コード、アセンブリ、文字列、名前、セグメント、callers、callees、xrefs、注釈 |
|
||||
| 合成解析 | 15 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, Swift / ObjC 検出 |
|
||||
| macOS ネイティブ | 7 | Mach-O メタデータ、署名、plist、アーキテクチャ、Swift デマングル。Hopper 起動不要 |
|
||||
| アーティファクトグラフ | 5 | ディレクトリと対応パッケージの調査、Interface Builder UI グラフ、Apple アセットカタログのメタデータ、明示選択された抽出 |
|
||||
| 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`、証拠バンドル、有限リプレイ状態機械、プロセス/成果物/関数比較、残余未知レジストリ |
|
||||
| ツール分類 | 数 | 用途 |
|
||||
| -------------------- | --: | --------------------------------------------------------------------------- |
|
||||
| バイナリ検査 | 39 | 関数、疑似コード、アセンブリ、文字列、シンボル、参照、注釈 |
|
||||
| 組み合わせた解析 | 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、VS Code に対応します。既存の REA 登録は初期選択され、それ以外の検出済みクライアントは選択が必要です。ローカル MCP サーバーに対応するエージェントは、次の設定でも接続できます。
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
@@ -173,7 +217,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.0", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,7 +237,7 @@ flowchart LR
|
||||
Ghidra --> App
|
||||
```
|
||||
|
||||
CLI と MCP サーバーは同じ解析エンジンを使います。ターミナルのコマンドは終了時にアプリを閉じ、エージェントのセッションは調査中アプリを開いたままにします。
|
||||
CLI と MCP サーバーは同じ解析処理を使います。ターミナルコマンドは完了後に自分のブリッジセッションを解放し、エージェントセッションは調査中の接続を維持できます。REA セッションを閉じても、ユーザーが利用中の Hopper は終了しません。
|
||||
|
||||
## CLI
|
||||
|
||||
@@ -229,7 +273,7 @@ REA は必要なときに Hopper を起動します。Hopper のランチャー
|
||||
|
||||
<details><summary><strong>Hopper を先に起動する必要がありますか?</strong></summary>
|
||||
|
||||
いいえ。REA が必要時に起動します。すでに起動している Hopper にも対応します。
|
||||
いいえ。REA が必要時に起動します。Hopper が起動済みでも使えますが、既存の GUI ドキュメントには接続せず、新しい解析ドキュメントを開きます。
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
+120
-75
@@ -4,7 +4,7 @@
|
||||
|
||||
# REA: 무엇이든 리버스 엔지니어링
|
||||
|
||||
### 코딩 에이전트가 무엇이든 리버스 엔지니어링할 수 있게 하는 하나의 CLI 및 MCP 서버
|
||||
### 에이전트로 앱의 동작부터 네이티브 바이너리까지 조사하세요.
|
||||
|
||||
**마음에 드는 기능을 찾고, 작동 방식을 이해하고, 원하는 방식으로 구현하세요.**
|
||||
|
||||
@@ -13,35 +13,50 @@
|
||||
[](#조사-도구-카탈로그)
|
||||
[](https://nodejs.org/)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
[빠른 시작](#빠른-시작) · [바이너리에서 동작까지](#바이너리에서-동작까지) · [도구 카탈로그](#조사-도구-카탈로그) · [작동 방식](#작동-방식) · [FAQ](#faq)
|
||||
[빠른 시작](#빠른-시작) · [현재 지원 범위](#현재-지원-범위) · [바이너리에서 동작까지](#바이너리에서-동작까지) · [조사 도구 카탈로그](#조사-도구-카탈로그) · [로드맵](#로드맵) · [작동 방식](#작동-방식)
|
||||
|
||||
<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 +68,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, 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.19+ 또는 24.11+와 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 +190,26 @@ REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이
|
||||
|
||||
## 조사 도구 카탈로그
|
||||
|
||||
| 도구 그룹 | 수 | 예시 |
|
||||
| ------------------- | --: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 바이너리 검사 | 39 | 프로시저, 의사 코드, 어셈블리, 문자열, 이름, 세그먼트, callers, callees, xrefs, 주석 |
|
||||
| 합성 분석 | 15 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, Swift 및 ObjC 탐색 |
|
||||
| macOS 네이티브 도구 | 7 | Mach-O 메타데이터, 코드 서명, plist, 아키텍처, Swift 디맹글링. Hopper 실행 불필요 |
|
||||
| 아티팩트 그래프 | 5 | 디렉터리와 지원 패키지 검사, Interface Builder UI 그래프, Apple 에셋 카탈로그 메타데이터, 명시적으로 선택한 추출 |
|
||||
| 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 상태 머신, 프로세스·아티팩트·함수 비교, 잔여 미지수 레지스트리 |
|
||||
| 도구 분류 | 수 | 용도 |
|
||||
| ----------------- | --: | -------------------------------------------------------------------- |
|
||||
| 바이너리 검사 | 39 | 함수, 의사 코드, 어셈블리, 문자열, 심볼, 참조, 주석 |
|
||||
| 결합 분석 | 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, VS Code를 지원합니다. 기존 REA 등록은 기본 선택되고, 그 밖의 감지된 클라이언트는 직접 선택해야 합니다. 로컬 MCP 서버를 지원하는 에이전트는 다음 설정으로 연결할 수 있습니다.
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
@@ -173,7 +218,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.0", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,7 +238,7 @@ flowchart LR
|
||||
Ghidra --> App
|
||||
```
|
||||
|
||||
CLI와 MCP 서버는 같은 분석 엔진을 사용합니다. 터미널 명령은 끝날 때 앱을 닫고, 에이전트 세션은 조사하는 동안 앱을 열어 둡니다.
|
||||
CLI와 MCP 서버는 같은 분석 워크플로를 사용합니다. 터미널 명령은 완료 후 자신의 브리지 세션을 해제하고, 에이전트 세션은 조사 중 연결을 유지할 수 있습니다. REA 세션을 닫아도 사용자가 이용 중인 Hopper 앱은 종료하지 않습니다.
|
||||
|
||||
## CLI
|
||||
|
||||
@@ -229,7 +274,7 @@ REA는 필요할 때 Hopper를 시작합니다. Hopper 런처는 내부적으로
|
||||
|
||||
<details><summary><strong>Hopper를 미리 실행해야 하나요?</strong></summary>
|
||||
|
||||
아니요. REA가 필요할 때 시작하며 이미 실행 중인 Hopper도 지원합니다.
|
||||
아니요. REA가 필요할 때 시작합니다. Hopper가 실행 중이어도 사용할 수 있지만 기존 GUI 문서에 연결하는 대신 새 분석 문서를 엽니다.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
+120
-76
@@ -4,7 +4,7 @@
|
||||
|
||||
# REA:逆向工程一切
|
||||
|
||||
### 一个 CLI 与 MCP 服务器,让编程智能体逆向工程任何程序
|
||||
### 使用智能体逆向工程任何程序,从应用行为到原生二进制文件。
|
||||
|
||||
**看到喜欢的功能。理解它的原理。按照你的方式实现。**
|
||||
|
||||
@@ -13,35 +13,49 @@
|
||||
[](#调查工具目录)
|
||||
[](https://nodejs.org/)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
[快速开始](#快速开始) · [从二进制到行为](#从二进制到行为) · [工具目录](#调查工具目录) · [工作原理](#工作原理) · [常见问题](#常见问题)
|
||||
[快速开始](#快速开始) · [当前状态](#当前状态) · [从二进制到行为](#从二进制到行为) · [调查工具目录](#调查工具目录) · [路线图](#路线图) · [工作原理](#工作原理)
|
||||
|
||||
<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 +67,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 和 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.19+ 或 24.11+,以及 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 +189,26 @@ REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常
|
||||
|
||||
## 调查工具目录
|
||||
|
||||
| 工具类别 | 数量 | 示例 |
|
||||
| ----------------- | ---: | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 二进制检查 | 39 | 过程、伪代码、汇编、字符串、名称、段、调用者、被调用者、交叉引用、注释 |
|
||||
| 组合分析 | 15 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`、Swift 与 ObjC 发现 |
|
||||
| macOS 原生工具 | 7 | Mach-O 元数据、代码签名、plist、架构与 Swift 符号还原,无需启动 Hopper |
|
||||
| 制品图谱 | 5 | 目录及支持的软件包内联检查、Interface Builder UI 图解、Apple 资源目录元数据分页检查,以及显式选择的事务式提取 |
|
||||
| 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`、证据包、有限重放状态机、进程、制品与函数比较、残余未知项注册表 |
|
||||
| 工具类别 | 数量 | 用途 |
|
||||
| ----------------- | ---: | --------------------------------------------------------- |
|
||||
| 二进制检查 | 39 | 函数、伪代码、汇编、字符串、符号、段、引用与注释 |
|
||||
| 组合分析 | 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 和 VS Code。已有 REA 注册默认选中;其他检测到的客户端需手动选择。任何支持本地 MCP 服务器的智能体都可以使用以下配置连接 REA。
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
@@ -173,7 +217,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.0", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,7 +237,7 @@ flowchart LR
|
||||
Ghidra --> App
|
||||
```
|
||||
|
||||
CLI 与 MCP 服务器使用相同的分析引擎。终端命令完成后会关闭应用;智能体会话则会在调查期间保持应用打开。
|
||||
CLI 与 MCP 服务器使用相同的分析流程。终端命令完成后释放自己创建的桥接会话;智能体会话可以在调查期间保持连接。关闭 REA 会话不会退出用户正在使用的 Hopper 应用。
|
||||
|
||||
## CLI
|
||||
|
||||
@@ -223,7 +267,7 @@ REA 会推导明确的格式和架构参数,以避免常见的 FAT 与 ARM 选
|
||||
|
||||
## 安全模型
|
||||
|
||||
每个桥会话都使用随机能力令牌和仅限当前用户的 Unix 套接字。协议消息有大小限制,面向调用者的错误不会暴露启动器 stderr 或内部异常原因。Ghidra 会话还使用隔离的临时项目,不会打开或修改用户自己的 Ghidra 项目。
|
||||
每个桥会话都使用随机能力令牌和仅限当前用户的 Unix 套接字。诊断保留本机路径、摘要和错误位置等排查信息,同时移除凭据和认证令牌。Ghidra 会话还使用隔离的临时项目,不会打开或修改用户自己的 Ghidra 项目。
|
||||
|
||||
这不是沙箱,也无法防御以同一操作系统用户身份运行的恶意进程。打开不可信二进制文件会让所选本地提供商以当前用户权限进行解析和分析。请按照 [SECURITY.md](SECURITY.md) 中的私密流程报告漏洞。
|
||||
|
||||
@@ -231,7 +275,7 @@ REA 会推导明确的格式和架构参数,以避免常见的 FAT 与 ARM 选
|
||||
|
||||
<details><summary><strong>Hopper 是否需要提前运行?</strong></summary>
|
||||
|
||||
不需要。REA 会在操作需要时启动 Hopper,也支持已经运行的 Hopper。
|
||||
不需要。REA 会在操作需要时启动 Hopper。Hopper 已经运行时也可以使用,但 REA 会打开新的分析文档,不会连接并复用现有 GUI 文档。
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -48,9 +48,13 @@ import ghidra.app.decompiler.DecompInterface;
|
||||
import ghidra.app.decompiler.DecompileOptions;
|
||||
import ghidra.app.decompiler.DecompileResults;
|
||||
import ghidra.app.decompiler.DecompiledFunction;
|
||||
import ghidra.app.decompiler.ClangCaseToken;
|
||||
import ghidra.app.decompiler.ClangNode;
|
||||
import ghidra.framework.Application;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.address.AddressSpace;
|
||||
import ghidra.program.model.address.AddressRange;
|
||||
import ghidra.program.model.address.AddressRangeIterator;
|
||||
import ghidra.program.model.block.BasicBlockModel;
|
||||
import ghidra.program.model.block.CodeBlock;
|
||||
import ghidra.program.model.block.CodeBlockIterator;
|
||||
@@ -82,6 +86,8 @@ import ghidra.program.model.pcode.HighSymbol;
|
||||
import ghidra.program.model.pcode.JumpTable;
|
||||
import ghidra.program.model.pcode.PcodeOp;
|
||||
import ghidra.program.model.pcode.PcodeOpAST;
|
||||
import ghidra.program.model.pcode.PcodeBlock;
|
||||
import ghidra.program.model.pcode.PcodeBlockBasic;
|
||||
import ghidra.program.model.pcode.Varnode;
|
||||
import ghidra.program.model.pcode.VarnodeAST;
|
||||
|
||||
@@ -372,6 +378,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
decompiler.setOptions(options);
|
||||
decompiler.toggleCCode(true);
|
||||
decompiler.toggleSyntaxTree(true);
|
||||
decompiler.toggleJumpLoads(true);
|
||||
if (!decompiler.openProgram(currentProgram)) {
|
||||
throw new IllegalStateException("Ghidra decompiler could not open the imported Program");
|
||||
}
|
||||
@@ -440,7 +447,9 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
result.addProperty("name", procedureName(function));
|
||||
result.addProperty("entrypoint", canonicalAddress(function.getEntryPoint()));
|
||||
result.addProperty("basicblock_count", basicBlocks(function).size());
|
||||
result.addProperty("length", function.getBody().getNumAddresses());
|
||||
JsonObject body = functionBody(function);
|
||||
result.addProperty("length", body.get("total_bytes").getAsLong());
|
||||
result.add("body", body);
|
||||
result.addProperty("signature", function.getPrototypeString(false, true));
|
||||
result.add("locals", functionLocals(function));
|
||||
result.add("classification", functionClassification(function));
|
||||
@@ -774,6 +783,10 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
limitations.add(
|
||||
"Synthetic Ghidra entry-point references without actionable memory sources are omitted."
|
||||
);
|
||||
if (decompilation != null && decompilation.getErrorMessage() != null &&
|
||||
!decompilation.getErrorMessage().isBlank()) {
|
||||
limitations.add("Ghidra decompiler diagnostic: " + decompilation.getErrorMessage());
|
||||
}
|
||||
result.add("limitations", limitations);
|
||||
return result;
|
||||
}
|
||||
@@ -1346,8 +1359,12 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
}
|
||||
|
||||
JumpTable[] recoveredJumpTables = highFunction.getJumpTables();
|
||||
List<ClangNode> caseTokens = new ArrayList<>();
|
||||
if (decompilation.getCCodeMarkup() != null) {
|
||||
decompilation.getCCodeMarkup().flatten(caseTokens);
|
||||
}
|
||||
for (int index = 0; index < recoveredJumpTables.length; index += 1) {
|
||||
jumpTables.add(inferredJumpTable(recoveredJumpTables[index]));
|
||||
jumpTables.add(inferredJumpTable(recoveredJumpTables[index], caseTokens));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1443,7 +1460,10 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return result;
|
||||
}
|
||||
|
||||
private JsonObject inferredJumpTable(JumpTable jumpTable) throws Exception {
|
||||
private JsonObject inferredJumpTable(
|
||||
JumpTable jumpTable,
|
||||
List<ClangNode> caseTokens
|
||||
) throws Exception {
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty(
|
||||
"dispatch_address",
|
||||
@@ -1542,13 +1562,33 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
}
|
||||
result.add("data_sources", dataSources);
|
||||
Address[] targets = jumpTable.getCases();
|
||||
Integer[] labels = jumpTable.getLabelValues();
|
||||
boolean labelsAligned = labels != null && labels.length == targets.length;
|
||||
TypedJumpTableCases typedCases = typedJumpTableCases(jumpTable, caseTokens);
|
||||
JsonArray mappings = new JsonArray();
|
||||
JsonArray defaultTargets = new JsonArray();
|
||||
for (Address target : typedCases.defaults) {
|
||||
defaultTargets.add(typedJumpTableTarget(target, jumpTable, "Typed default label"));
|
||||
}
|
||||
Set<Address> coveredTargets = new HashSet<>(typedCases.cases.values());
|
||||
coveredTargets.addAll(typedCases.defaults);
|
||||
if (recoveredAArch64 != null) {
|
||||
for (int index = 0; index < recoveredAArch64.mappings.size(); index += 1) {
|
||||
monitor.checkCancelled();
|
||||
RecoveredAArch64Mapping recovered = recoveredAArch64.mappings.get(index);
|
||||
if (
|
||||
typedCases.unknownTargets.contains(recovered.targetAddress) ||
|
||||
typedCases.ambiguousValues.contains((long) recovered.caseValue)
|
||||
) continue;
|
||||
Address typedTarget = typedCases.cases.get((long) recovered.caseValue);
|
||||
if (typedTarget != null && !typedTarget.equals(recovered.targetAddress)) {
|
||||
typedCases.cases.remove((long) recovered.caseValue);
|
||||
typedCases.ambiguousValues.add((long) recovered.caseValue);
|
||||
typedCases.unknownTargets.add(typedTarget);
|
||||
typedCases.unknownTargets.add(recovered.targetAddress);
|
||||
typedCases.limitations.add(
|
||||
"Typed case evidence and the verified relative table disagree on a destination; both destinations remain unknown for that case."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
JsonObject mapping = new JsonObject();
|
||||
mapping.addProperty("case_value", recovered.caseValue);
|
||||
String targetAddress = canonicalAddress(recovered.targetAddress);
|
||||
@@ -1566,57 +1606,60 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
)
|
||||
);
|
||||
mapping.add("evidence", evidence);
|
||||
mappings.add(mapping);
|
||||
if (!typedCases.cases.containsKey((long) recovered.caseValue)) {
|
||||
mappings.add(mapping);
|
||||
}
|
||||
coveredTargets.add(recovered.targetAddress);
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (Map.Entry<Long, Address> entry : typedCases.cases.entrySet()) {
|
||||
JsonObject mapping = typedJumpTableTarget(
|
||||
entry.getValue(),
|
||||
jumpTable,
|
||||
"Typed case value " + entry.getKey()
|
||||
);
|
||||
mapping.addProperty("case_value", entry.getKey());
|
||||
mappings.add(mapping);
|
||||
}
|
||||
Set<Address> emittedUnknownTargets = new HashSet<>();
|
||||
for (int index = 0; index < targets.length; index += 1) {
|
||||
monitor.checkCancelled();
|
||||
boolean unresolvedToken = typedCases.unknownTargets.contains(targets[index]);
|
||||
if (coveredTargets.contains(targets[index]) && !unresolvedToken) continue;
|
||||
if (!emittedUnknownTargets.add(targets[index])) continue;
|
||||
JsonObject mapping = new JsonObject();
|
||||
if (!labelsAligned || labels[index] == null) {
|
||||
mapping.add("case_value", JsonNull.INSTANCE);
|
||||
}
|
||||
else {
|
||||
mapping.addProperty("case_value", labels[index]);
|
||||
}
|
||||
mapping.add("case_value", JsonNull.INSTANCE);
|
||||
String targetAddress = canonicalAddress(targets[index]);
|
||||
mapping.addProperty("target_address", targetAddress);
|
||||
// Ghidra's recovered case-to-target relation does not identify
|
||||
// which backing load table produced an individual case. Keep the
|
||||
// table inventory and recovered mappings separate instead of
|
||||
// attaching every table address to every target.
|
||||
mapping.addProperty(
|
||||
"confidence",
|
||||
labelsAligned && labels[index] != null
|
||||
? "high"
|
||||
: "medium"
|
||||
);
|
||||
mapping.addProperty("confidence", "medium");
|
||||
JsonArray evidence = new JsonArray();
|
||||
evidence.add(
|
||||
inferenceEvidence(
|
||||
"jump-table",
|
||||
"ghidra-high-function",
|
||||
labelsAligned && labels[index] != null
|
||||
? "Ghidra paired case value " +
|
||||
labels[index] +
|
||||
" with target " +
|
||||
"Ghidra recovered target " +
|
||||
targetAddress +
|
||||
" at dispatch " +
|
||||
canonicalAddress(jumpTable.getSwitchAddress()) +
|
||||
"."
|
||||
: "Ghidra recovered target " +
|
||||
targetAddress +
|
||||
" at dispatch " +
|
||||
canonicalAddress(jumpTable.getSwitchAddress()) +
|
||||
", but no aligned case value was available."
|
||||
", but no unambiguous, exactly representable case label or default relationship was available."
|
||||
)
|
||||
);
|
||||
mapping.add("evidence", evidence);
|
||||
mappings.add(mapping);
|
||||
}
|
||||
}
|
||||
result.add("mappings", mappings);
|
||||
result.add("default_targets", defaultTargets);
|
||||
JsonArray limitations = new JsonArray();
|
||||
for (String limitation : typedCases.limitations) limitations.add(limitation);
|
||||
if (defaultTargets.isEmpty()) {
|
||||
limitations.add(
|
||||
"Ghidra exposed no typed default label with an unambiguous relationship to a recovered destination of this dispatch; default or out-of-range handling remains unknown."
|
||||
);
|
||||
}
|
||||
if (loadTables.length == 0 && referencedDataAddresses.isEmpty()) {
|
||||
limitations.add(
|
||||
"Ghidra recovered targets but exposed no backing load-table or dispatch-block data address."
|
||||
@@ -1631,9 +1674,9 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
"Load-table metadata was absent; table-level candidate ownership is inferred from typed data references in the dispatch block and its immediate control-flow predecessors, but candidates are not assigned to individual mappings."
|
||||
);
|
||||
}
|
||||
if (recoveredAArch64 == null && !labelsAligned) {
|
||||
if (!emittedUnknownTargets.isEmpty()) {
|
||||
limitations.add(
|
||||
"Case-label and target counts differ or labels are unavailable; all case values are left unknown because index alignment cannot be established."
|
||||
"Some recovered destinations lack an unambiguous, exactly representable case/default relationship; their case values remain unknown. Unequal label and destination arrays are never paired by index."
|
||||
);
|
||||
}
|
||||
if (recoveredAArch64 == null && loadTables.length == 0 && currentProgram.getLanguageID().getIdAsString().startsWith("AARCH64:")) {
|
||||
@@ -1648,6 +1691,124 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static final class TypedJumpTableCases {
|
||||
private final Map<Long, Address> cases = new TreeMap<>();
|
||||
private final Set<Address> defaults = new java.util.TreeSet<>();
|
||||
private final Set<Address> unknownTargets = new HashSet<>();
|
||||
private final Set<Long> ambiguousValues = new HashSet<>();
|
||||
private final Set<String> limitations = new java.util.LinkedHashSet<>();
|
||||
}
|
||||
|
||||
private TypedJumpTableCases typedJumpTableCases(
|
||||
JumpTable jumpTable,
|
||||
List<ClangNode> tokens
|
||||
) throws Exception {
|
||||
TypedJumpTableCases result = new TypedJumpTableCases();
|
||||
Set<Address> targets = new HashSet<>();
|
||||
for (Address target : jumpTable.getCases()) targets.add(target);
|
||||
for (ClangNode node : tokens) {
|
||||
monitor.checkCancelled();
|
||||
if (!(node instanceof ClangCaseToken token)) continue;
|
||||
PcodeOp operation = token.getPcodeOp();
|
||||
if (operation == null || operation.getParent() == null) continue;
|
||||
PcodeBlockBasic block = operation.getParent();
|
||||
Address target = block.getStart();
|
||||
if (!targets.contains(target)) continue;
|
||||
PcodeOp dispatch = null;
|
||||
boolean ambiguousDispatch = false;
|
||||
boolean includesDispatch = false;
|
||||
for (int index = 0; index < block.getInSize(); index += 1) {
|
||||
PcodeBlock predecessor = block.getIn(index);
|
||||
if (!(predecessor instanceof PcodeBlockBasic basic)) continue;
|
||||
PcodeOp last = basic.getLastOp();
|
||||
if (last == null || last.getOpcode() != PcodeOp.BRANCHIND) continue;
|
||||
if (last.getSeqnum().getTarget().equals(jumpTable.getSwitchAddress())) {
|
||||
includesDispatch = true;
|
||||
}
|
||||
if (dispatch != null && dispatch != last) ambiguousDispatch = true;
|
||||
dispatch = last;
|
||||
}
|
||||
if (!includesDispatch) continue;
|
||||
if (ambiguousDispatch) {
|
||||
result.unknownTargets.add(target);
|
||||
result.limitations.add(
|
||||
"A typed case/default block has multiple indirect dispatch predecessors; REA cannot attribute its label to one switch."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// Default is a typed label role, not an OFF sentinel: Ghidra can
|
||||
// encode a default token with an ordinary in-range case value.
|
||||
if ("default".equals(token.getText())) {
|
||||
result.defaults.add(target);
|
||||
continue;
|
||||
}
|
||||
Long value = exactTypedCaseValue(token);
|
||||
if (value == null) {
|
||||
result.unknownTargets.add(target);
|
||||
result.limitations.add(
|
||||
"Typed case label " + token.getText() +
|
||||
" has encoded unsigned magnitude " + Long.toUnsignedString(token.getValue()) +
|
||||
" at recovered target " + canonicalAddress(target) +
|
||||
" for dispatch " + canonicalAddress(jumpTable.getSwitchAddress()) +
|
||||
"; the label is nonliteral, inconsistent with its encoded magnitude, or outside the exact JSON integer range, so its numeric case value remains unknown."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (result.ambiguousValues.contains(value)) {
|
||||
result.unknownTargets.add(target);
|
||||
continue;
|
||||
}
|
||||
Address previous = result.cases.putIfAbsent(value, target);
|
||||
if (previous != null && !previous.equals(target)) {
|
||||
result.cases.remove(value);
|
||||
result.ambiguousValues.add(value);
|
||||
result.unknownTargets.add(previous);
|
||||
result.unknownTargets.add(target);
|
||||
result.limitations.add(
|
||||
"Typed tokens assign one case value to different recovered destinations; that case remains unknown."
|
||||
);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Long exactTypedCaseValue(ClangCaseToken token) {
|
||||
String text = token.getText();
|
||||
if (text == null || !text.matches("-?(?:0[xX][0-9a-fA-F]+|[0-9]+)")) return null;
|
||||
boolean negative = text.startsWith("-");
|
||||
String literal = negative ? text.substring(1) : text;
|
||||
BigInteger magnitude = literal.startsWith("0x") || literal.startsWith("0X")
|
||||
? new BigInteger(literal.substring(2), 16)
|
||||
: new BigInteger(literal, 10);
|
||||
// Negative numeric tokens carry an unsigned magnitude in OFF, so
|
||||
// getValue()/getScalar() alone would incorrectly emit positive cases.
|
||||
if (!magnitude.equals(new BigInteger(Long.toUnsignedString(token.getValue())))) return null;
|
||||
BigInteger value = negative ? magnitude.negate() : magnitude;
|
||||
if (value.abs().compareTo(BigInteger.valueOf(9007199254740991L)) > 0) return null;
|
||||
return value.longValueExact();
|
||||
}
|
||||
|
||||
private static JsonObject typedJumpTableTarget(
|
||||
Address target,
|
||||
JumpTable table,
|
||||
String label
|
||||
) {
|
||||
JsonObject result = new JsonObject();
|
||||
String address = canonicalAddress(target);
|
||||
result.addProperty("target_address", address);
|
||||
result.addProperty("confidence", "high");
|
||||
JsonArray evidence = new JsonArray();
|
||||
evidence.add(inferenceEvidence(
|
||||
"jump-table",
|
||||
"ghidra-clang-case-token",
|
||||
label + " belongs to the p-code block starting at recovered target " +
|
||||
address + "; its unique indirect dispatch predecessor is " +
|
||||
canonicalAddress(table.getSwitchAddress()) + "."
|
||||
));
|
||||
result.add("evidence", evidence);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static final class RecoveredAArch64Mapping {
|
||||
private final int caseValue;
|
||||
private final Address targetAddress;
|
||||
@@ -2351,9 +2512,67 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
result.addProperty("address", canonicalAddress(function.getEntryPoint()));
|
||||
result.addProperty("name", procedureName(function));
|
||||
result.add("classification", functionClassification(function));
|
||||
result.add("body", functionBody(function));
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Observe the entire AddressSet; range endpoints are inclusive, not a guessed span. */
|
||||
private static JsonObject functionBody(Function function) {
|
||||
JsonArray ranges = new JsonArray();
|
||||
BigInteger total = BigInteger.ZERO;
|
||||
Address first = null;
|
||||
Address last = null;
|
||||
AddressSpace space = null;
|
||||
boolean multipleSpaces = false;
|
||||
AddressRangeIterator iterator = function.getBody().getAddressRanges(true);
|
||||
while (iterator.hasNext()) {
|
||||
AddressRange range = iterator.next();
|
||||
Address start = range.getMinAddress();
|
||||
Address end = range.getMaxAddress();
|
||||
if (start.getAddressSpace().getAddressableUnitSize() != 1) {
|
||||
throw new IllegalStateException("Function body byte coverage requires a byte-addressed space");
|
||||
}
|
||||
JsonObject value = new JsonObject();
|
||||
value.addProperty("start", canonicalAddress(start));
|
||||
value.addProperty("end", canonicalAddress(end));
|
||||
ranges.add(value);
|
||||
BigInteger begin = new BigInteger(Long.toUnsignedString(start.getOffset()));
|
||||
BigInteger finish = new BigInteger(Long.toUnsignedString(end.getOffset()));
|
||||
total = total.add(finish.subtract(begin).add(BigInteger.ONE));
|
||||
if (first == null) {
|
||||
first = start;
|
||||
space = start.getAddressSpace();
|
||||
} else if (!space.equals(start.getAddressSpace())) {
|
||||
multipleSpaces = true;
|
||||
}
|
||||
last = end;
|
||||
}
|
||||
JsonObject result = new JsonObject();
|
||||
result.addProperty("available", true);
|
||||
result.addProperty("provenance", "ghidra-function-body-address-set");
|
||||
result.add("ranges", ranges);
|
||||
result.addProperty("total_bytes", safeBodyByteCount(total));
|
||||
if (multipleSpaces) {
|
||||
result.add("span_bytes", JsonNull.INSTANCE);
|
||||
} else {
|
||||
BigInteger span = first == null ? BigInteger.ZERO :
|
||||
new BigInteger(Long.toUnsignedString(last.getOffset()))
|
||||
.subtract(new BigInteger(Long.toUnsignedString(first.getOffset())))
|
||||
.add(BigInteger.ONE);
|
||||
result.addProperty("span_bytes", safeBodyByteCount(span));
|
||||
}
|
||||
result.addProperty("non_contiguous", ranges.size() > 1);
|
||||
result.addProperty("contains_entry", function.getBody().contains(function.getEntryPoint()));
|
||||
return result;
|
||||
}
|
||||
|
||||
private static long safeBodyByteCount(BigInteger value) {
|
||||
if (value.signum() < 0 || value.compareTo(BigInteger.valueOf(9007199254740991L)) > 0) {
|
||||
throw new IllegalStateException("Function body byte coverage exceeds exact JSON integer range");
|
||||
}
|
||||
return value.longValueExact();
|
||||
}
|
||||
|
||||
private static String exclusiveEnd(MemoryBlock block) {
|
||||
Address next = block.getEnd().next();
|
||||
if (next != null && next.getAddressSpace().equals(block.getStart().getAddressSpace())) {
|
||||
|
||||
+25
-2
@@ -241,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",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -309,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,
|
||||
}
|
||||
|
||||
@@ -553,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,
|
||||
@@ -764,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:
|
||||
|
||||
@@ -26,13 +26,13 @@ func childCount(_ element: AXUIElement) -> Int {
|
||||
var count = 0
|
||||
return AXUIElementGetAttributeValueCount(element, kAXChildrenAttribute as CFString, &count) == .success ? count : 0
|
||||
}
|
||||
func children(_ element: AXUIElement, limit: Int = 2000) -> [AXUIElement] {
|
||||
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).map { String($0.prefix(4096)) } ?? NSNull() as Any }
|
||||
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 }
|
||||
@@ -108,14 +108,15 @@ func observe(_ request: Request) async throws -> [String: Any] {
|
||||
var pending: [(AXUIElement, [Int])] = [(root, [])]
|
||||
while let (element, path) = pending.popLast() {
|
||||
if nodes.count >= request.max_nodes { truncated = true; break }
|
||||
let items = children(element)
|
||||
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": childCount(element)])
|
||||
if path.count >= 32 { if !items.isEmpty { truncated = true }; continue }
|
||||
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, items.count)
|
||||
if childCount(element) > count { truncated = true }
|
||||
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])) }
|
||||
}
|
||||
}
|
||||
@@ -133,8 +134,8 @@ func observe(_ request: Request) async throws -> [String: Any] {
|
||||
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: [:]), png.count <= 8 * 1024 * 1024 else {
|
||||
try fail("image-limit", "Selected-window PNG exceeded 8 MiB")
|
||||
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]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; Windows analysis blocked |
|
||||
| [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,7 +24,6 @@ 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"]
|
||||
end
|
||||
|
||||
subgraph "Provider Process Foundation"
|
||||
@@ -40,7 +39,7 @@ 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"
|
||||
|
||||
@@ -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,105 @@
|
||||
# DOS MZ analysis with Ghidra
|
||||
|
||||
REA can analyze DOS MZ executables 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
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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.
|
||||
+116
-87
@@ -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:
|
||||
|
||||
@@ -35,7 +40,7 @@ REA supports Node.js 22.19+ and 24.11+ (including newer releases). It uses the n
|
||||
|
||||
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,64 @@ 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` |
|
||||
| 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 +126,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 +147,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 +212,22 @@ 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 or macOS x64/arm64.
|
||||
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 22 read-only operations: ten inventory/name/search
|
||||
operations and twelve function-analysis operations. These cover metadata,
|
||||
decompilation, assembly, resolved calls, typed references, xrefs, function
|
||||
dossiers, instructions, and recovered data types. GUI controls and annotation
|
||||
changes require Hopper.
|
||||
|
||||
Windows Ghidra operations are currently unavailable. The adapter reports
|
||||
`unsupported_host` until verified Job Object process ownership, private runtime
|
||||
DACLs, and reparse-safe path admission are implemented. The
|
||||
[Windows Ghidra P0 guide](windows-ghidra-p0.md) describes the intended boundary
|
||||
and remaining controls.
|
||||
|
||||
Extract Ghidra and install the JDK outside REA, then export absolute paths:
|
||||
|
||||
@@ -212,7 +238,7 @@ rea doctor --json
|
||||
rea setup
|
||||
```
|
||||
|
||||
PowerShell configuration for Windows uses the same non-secret paths:
|
||||
For Windows diagnostics, use the same installation paths in PowerShell. These settings do not enable the blocked analysis operations:
|
||||
|
||||
```powershell
|
||||
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
|
||||
@@ -221,10 +247,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, and agent registration does not enable blocked Ghidra
|
||||
operations. 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 +265,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, but missing
|
||||
native ownership and path controls keep Windows operations unavailable.
|
||||
|
||||
Operations begin only after default auto-analysis completes. The provider
|
||||
startup deadline fails the open rather than exposing partial analysis. One
|
||||
@@ -256,8 +282,9 @@ remain unknown, reference-kind provenance is preserved, and provider-specific
|
||||
pseudocode is never treated as original source or Hopper-equivalent text.
|
||||
|
||||
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 +294,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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -125,9 +125,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,7 @@ 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.
|
||||
|
||||
+43
-58
@@ -9,15 +9,13 @@ 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
|
||||
operations callable for the current target, provider, 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`.
|
||||
optional negotiated client features plus the currently missing features.
|
||||
Opening or closing a target or observing a provider health transition emits
|
||||
`notifications/tools/list_changed`.
|
||||
|
||||
`binary_session.analysis_provider_candidates` is authoritative for deep-engine
|
||||
discovery. Target-free discovery is sorted by provider ID, reports host
|
||||
@@ -57,8 +55,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 +77,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.
|
||||
|
||||
@@ -25,7 +25,7 @@ coverage and unknowns. The CLI and MCP use the same application workflows.
|
||||
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
|
||||
<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
|
||||
@@ -96,41 +96,62 @@ 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.
|
||||
|
||||
## Approved native desktop observation
|
||||
## Native desktop observation
|
||||
|
||||
`observe_native_ui` captures one explicit existing PID/window ID after
|
||||
`observation_approved: true`. Screenshots use a selected-window ScreenCaptureKit
|
||||
`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` additionally requires `actions_approved: true` and
|
||||
`restore: "leave-as-is"`. Steps select AX child-index paths for press, increment/
|
||||
`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. The caller must choose whether to recover app state.
|
||||
post-action capture fails. Application state is left as-is; REA does not attempt
|
||||
to restore it.
|
||||
|
||||
Scenarios allow 16 steps, 30 seconds of total waits, 2,000 AX nodes per capture,
|
||||
8 MiB PNGs and 64 MiB output within a 180-second deadline. 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. Approved
|
||||
UI actions may change application data or trigger network activity.
|
||||
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
|
||||
|
||||
Ghidra 12.1.4 with a full 64-bit JDK 21 is bring-your-own. Linux x64 and macOS
|
||||
x64/arm64 are admitted; macOS requires the matching native decompiler. Windows
|
||||
x64 P0 remains limited to approved native PE applications. There is no GUI or
|
||||
mutation authority and no automatic fallback to Hopper.
|
||||
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. Windows Ghidra analysis is unavailable until
|
||||
Job Object process ownership, private runtime DACLs, and reparse-safe path checks
|
||||
are implemented and verified. See [Windows Ghidra P0](windows-ghidra-p0.md) and
|
||||
[issue #527](https://github.com/morluto/rea/issues/527).
|
||||
Ghidra has no GUI or mutation 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.
|
||||
@@ -138,8 +159,12 @@ mutation authority and no automatic fallback to Hopper.
|
||||
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, selected capture
|
||||
or a reported OS permission denial, changed-target rejection and cleanup.
|
||||
- `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
|
||||
|
||||
+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.
|
||||
|
||||
+41
-33
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"package": {
|
||||
"name": "rea-agents",
|
||||
"version": "3.2.1",
|
||||
"version": "4.0.0",
|
||||
"sdk": {
|
||||
"server": "2.0.0",
|
||||
"client": "2.0.0",
|
||||
"core": "2.0.0"
|
||||
},
|
||||
"skill_version": "23"
|
||||
"skill_version": "24"
|
||||
},
|
||||
"tools": {
|
||||
"total": 122,
|
||||
"total": 116,
|
||||
"families": [
|
||||
{
|
||||
"id": "direct",
|
||||
@@ -61,13 +61,12 @@
|
||||
{
|
||||
"id": "enhanced",
|
||||
"surface": "enhanced",
|
||||
"count": 15,
|
||||
"count": 14,
|
||||
"tools": [
|
||||
"analyze_function",
|
||||
"analyze_swift_types",
|
||||
"batch_decompile",
|
||||
"binary_overview",
|
||||
"find_code_for_string",
|
||||
"find_xrefs_to_name",
|
||||
"get_call_graph",
|
||||
"get_objc_classes",
|
||||
@@ -109,14 +108,13 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
@@ -161,16 +159,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"
|
||||
]
|
||||
@@ -178,7 +173,7 @@
|
||||
{
|
||||
"id": "session",
|
||||
"surface": "session",
|
||||
"count": 22,
|
||||
"count": 21,
|
||||
"tools": [
|
||||
"binary_session",
|
||||
"build_call_path",
|
||||
@@ -198,7 +193,6 @@
|
||||
"list_unknowns",
|
||||
"open_binary",
|
||||
"record_unknown",
|
||||
"run_replay_machine",
|
||||
"update_unknown",
|
||||
"verify_reconstruction",
|
||||
"verify_unknown_resolution"
|
||||
@@ -344,7 +338,6 @@
|
||||
"capabilities": [
|
||||
"compare_managed_members",
|
||||
"import_managed_reconstruction",
|
||||
"plan_managed_runtime_correlation",
|
||||
"project_managed_application_graph",
|
||||
"verify_managed_native_boundaries"
|
||||
]
|
||||
@@ -365,9 +358,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"
|
||||
]
|
||||
@@ -440,12 +430,36 @@
|
||||
{
|
||||
"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": "vscode",
|
||||
"display_name": "VS Code",
|
||||
"format": "vscode",
|
||||
"configuration": "managed"
|
||||
}
|
||||
],
|
||||
"cli": {
|
||||
"primary_count": 75,
|
||||
"primary_count": 69,
|
||||
"commands": [
|
||||
"analyze",
|
||||
"analyze-javascript-application",
|
||||
@@ -473,7 +487,6 @@
|
||||
"evaluate-reconstruction-coverage",
|
||||
"evidence-export",
|
||||
"evidence-import",
|
||||
"execute-node-characterization",
|
||||
"extract-artifact",
|
||||
"function",
|
||||
"import-managed-reconstruction",
|
||||
@@ -502,15 +515,10 @@
|
||||
"observe-javascript-runtime",
|
||||
"observe-native-ui",
|
||||
"observe-web-session",
|
||||
"plan-managed-runtime-correlation",
|
||||
"policy",
|
||||
"prepare-node-characterization",
|
||||
"project-managed-application-graph",
|
||||
"providers",
|
||||
"reconcile-javascript-runtime",
|
||||
"resolve-native-call-targets",
|
||||
"run-controlled-replay",
|
||||
"run-replay-machine",
|
||||
"search",
|
||||
"setup",
|
||||
"trace",
|
||||
@@ -519,7 +527,7 @@
|
||||
"trace-native-ui-action",
|
||||
"trace-native-values",
|
||||
"uninstall",
|
||||
"upgrade",
|
||||
"update",
|
||||
"verify-managed-native-boundaries",
|
||||
"xrefs"
|
||||
],
|
||||
@@ -532,15 +540,15 @@
|
||||
},
|
||||
"runtime_catalog": {
|
||||
"counts": {
|
||||
"cli_commands": 75,
|
||||
"mcp_tools": 122,
|
||||
"cli_commands": 69,
|
||||
"mcp_tools": 116,
|
||||
"mcp_prompts": 6
|
||||
},
|
||||
"digests": {
|
||||
"tools_sha256": "1a32398486b1e65f50a1a81de2d8867e6a4c296c165bf9994e53034b466c41b4",
|
||||
"prompts_sha256": "fa889fc671185ac9c968c6dc8309cf609e709cdc837dbbf39b7cdfadb84ac2d7",
|
||||
"combined_sha256": "a87e48bc626cb4361f494c074e71852304bfedee17fbba89976d32bb6244f230",
|
||||
"providers_sha256": "c5a88a49990fafc8244972ee8ff543fc19995eb79db960da55f6a311e15340d1"
|
||||
"tools_sha256": "7849b6f39bed200cfbf5f4d09ba3f9e0661824efb5753ae2edde8ef2fb66535e",
|
||||
"prompts_sha256": "4762db348821fec787a5b8c6423f2c48a71f6f0ec2afee97b16ad69030ec504b",
|
||||
"combined_sha256": "5bb3f994a3c4d0c87a34ff5aab4e28141b5fa6f3a98b5ae7e95686d85d9d983e",
|
||||
"providers_sha256": "2ae8b85b20be338739e500549d275e78e875aaa10235d2ca283f0ade7787da90"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+48
-51
@@ -1,25 +1,21 @@
|
||||
# Static-analysis provider evaluation
|
||||
|
||||
Status: the Ghidra read-only analysis provider is shipped on Linux x64 and macOS x64/arm64 with matching native tools, and has
|
||||
an experimental Windows x64 P0 for approved native PE applications. 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 22 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 22 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.
|
||||
Ghidra does not expose Hopper's GUI or annotation operations.
|
||||
|
||||
Windows Ghidra analysis is unavailable, including on a correctly configured
|
||||
Ghidra/JDK host. Job Object process ownership, private runtime DACLs, and
|
||||
reparse-safe path checks are not implemented. Package and adapter tests do not
|
||||
establish real Windows analysis support. 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,
|
||||
@@ -31,8 +27,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
|
||||
@@ -53,10 +49,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
|
||||
@@ -65,18 +61,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 22 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.
|
||||
@@ -125,24 +122,24 @@ 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 the Windows P0 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` remains blocked by the missing Windows native
|
||||
authority. After those controls are implemented, the lane must check the
|
||||
source-owned native x86-64 PE fixture, all read-only operations,
|
||||
target/snapshot/import SHA-256 linkage, and project, endpoint, process, and
|
||||
runtime cleanup. The existence of this verifier is not evidence that Windows
|
||||
Ghidra analysis works.
|
||||
|
||||
## 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
|
||||
@@ -152,12 +149,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 | 22 read-only operations shipped on Linux/macOS with installation checks, temporary projects, a serial API queue, and real ELF/PE/Mach-O conformance. Windows analysis is blocked; GUI and mutation 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.
|
||||
|
||||
+66
-30
@@ -1,32 +1,67 @@
|
||||
# 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, 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 ten inventory operations and twelve function-analysis
|
||||
operations, for 22 read-only operations total. 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.
|
||||
|
||||
Windows Ghidra operations remain unavailable until REA implements verified
|
||||
Job Object process ownership, private runtime DACLs, and reparse-safe path
|
||||
admission. Windows package and adapter tests do not establish a usable real
|
||||
Ghidra session. The [Windows P0 guide](windows-ghidra-p0.md) describes the
|
||||
intended boundary and remaining requirements.
|
||||
|
||||
## Ghidra maintenance boundary
|
||||
|
||||
@@ -36,16 +71,17 @@ 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
|
||||
Before enabling Windows Ghidra operations, 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.
|
||||
Hopper, and broad filesystem-sensitive workflows remain separate Windows
|
||||
projects rather than implied 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.
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ order rather than sorting or changing timestamps to conceal it.
|
||||
|
||||
Malformed archives, broken and cyclic references, unsupported metadata pointers,
|
||||
relative Objective-C method layouts, ambiguous call candidates, digest mismatch,
|
||||
cancellation, permission approval, bounded output and owned-resource cleanup
|
||||
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.
|
||||
|
||||
+106
-30
@@ -38,17 +38,22 @@ 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
|
||||
|
||||
@@ -95,6 +100,7 @@ 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 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 |
|
||||
@@ -104,11 +110,49 @@ 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).
|
||||
|
||||
## Apple Interface Builder archives
|
||||
|
||||
`npm run verify:interface-builder` compiles the source-owned AppKit XIB into a
|
||||
@@ -122,26 +166,57 @@ 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, without API HTML rendering |
|
||||
| `npm run docs:api:cached` | Explicit cached API HTML rendering |
|
||||
|
||||
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:
|
||||
|
||||
```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
|
||||
@@ -155,8 +230,8 @@ 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. TypeDoc rendering is a separate
|
||||
command and CI step.
|
||||
|
||||
Vitest and Node persistent compile caches are deliberately not enabled by
|
||||
default. To evaluate repeated local runs, opt in for both cold and warm
|
||||
@@ -190,8 +265,9 @@ 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
|
||||
|
||||
@@ -199,9 +275,9 @@ behavior also requires `npm run verify:package` and the applicable
|
||||
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 approved actions.
|
||||
Permission denial fails the positive lane. `npm run verify:native-ui:permissions`
|
||||
allows a permission-boundary-only result and explicitly reports
|
||||
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)
|
||||
|
||||
+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.
|
||||
|
||||
|
||||
@@ -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_fcfc6a2f890ccb4a5a7d19dce941b8f7644f05babd89e223100ef7b8b7a55afc",
|
||||
"manifest_id": "ecm_7da021af56cf2eb25489ed7ce494bfe92244e791fa3b5ad5a238a5eb14a23d9a",
|
||||
"verifier": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
@@ -13,7 +13,7 @@
|
||||
"skill_digests": [
|
||||
{
|
||||
"skill_id": "reverse-engineer-anything",
|
||||
"sha256": "6a884e8e2df17da0df0e8fda01aa3d66726fd7454c5606752c2b3ba3aa9e5449"
|
||||
"sha256": "411d5b61e237e3b9b2cb9b3a987d83a36134a2ea1810ddd8d9f695dc05709f5b"
|
||||
}
|
||||
],
|
||||
"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": {
|
||||
|
||||
+26
-17
@@ -1,23 +1,29 @@
|
||||
# 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 and currently unavailable. The production adapter reports
|
||||
`unsupported_host` because verified Job Object process ownership, private
|
||||
runtime DACLs, and reparse-safe path admission are not implemented. Installation
|
||||
paths or manual MCP registration do not enable operations.
|
||||
|
||||
This guide describes the intended P0 boundary and adapter mechanisms. It does
|
||||
not establish general Windows REA support. Even after the required controls
|
||||
are implemented, hostile, sensitive, security-decision, and concurrently
|
||||
mutable targets require separate acceptance.
|
||||
|
||||
## Supported boundary
|
||||
|
||||
The P0 accepts exactly:
|
||||
Once the required native controls are implemented, the intended P0 is limited to:
|
||||
|
||||
- a Windows x64 host;
|
||||
- Node.js 22.19+ or 24.11+;
|
||||
- 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 22 read-only Ghidra inventory 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
|
||||
Hopper, Ghidra GUI state, mutation, 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.
|
||||
|
||||
@@ -27,7 +33,7 @@ another package on Windows. The production adapter uses the packaged Java
|
||||
|
||||
## Configuration
|
||||
|
||||
Install REA, Ghidra, and JDK 21 separately. In PowerShell:
|
||||
For installation diagnostics, configure REA, Ghidra, and JDK 21 in PowerShell. Analysis remains blocked:
|
||||
|
||||
```powershell
|
||||
npm install --global rea-agents
|
||||
@@ -42,7 +48,9 @@ rea inspect "C:\approved-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.
|
||||
major version. `rea setup` can configure supported agent integrations and the
|
||||
bundled REA skill on Windows after approval. It does not install Hopper, Ghidra,
|
||||
or Java, and configuring an agent does not enable the blocked Ghidra operations.
|
||||
|
||||
For an MCP client, register the resolved Node entry point and preserve the
|
||||
three environment variables above. A representative configuration is:
|
||||
@@ -135,7 +143,7 @@ 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:
|
||||
The controlled real-engine lane requires the missing native authority and currently remains blocked:
|
||||
|
||||
```powershell
|
||||
$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
|
||||
@@ -144,13 +152,14 @@ npm ci
|
||||
npm run verify:ghidra:windows
|
||||
```
|
||||
|
||||
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
|
||||
The verifier requires the native authority before opening Ghidra. Its intended
|
||||
checks include a deterministic x86-64 PE fixture, the read-only operation
|
||||
contract, target/snapshot/import digest linkage, and 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`.
|
||||
|
||||
Passing this verifier proves the bounded P0 operation claim. It does not close
|
||||
the DACL, reparse-point, named-pipe, or Job Object gates above.
|
||||
Build, package, or adapter-test success does not prove the P0 operation claim.
|
||||
Real-engine acceptance requires the native controls and the separate
|
||||
verification lane; broader target and IPC security requirements remain distinct.
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
{
|
||||
"$schema": "./node_modules/knip/schema.json",
|
||||
"entry": ["src/main.ts", "src/cli.ts", "src/contracts/errorSchemas.ts"],
|
||||
"entry": [
|
||||
"src/main.ts",
|
||||
"src/cli.ts",
|
||||
"src/contracts/errorSchemas.ts",
|
||||
"scripts/run-development-tests.mjs"
|
||||
],
|
||||
"project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"],
|
||||
"ignoreDependencies": ["electron"],
|
||||
"ignoreBinaries": ["ps"],
|
||||
|
||||
Generated
+407
-19
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "rea-agents",
|
||||
"version": "3.2.1",
|
||||
"version": "4.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "rea-agents",
|
||||
"version": "3.2.1",
|
||||
"version": "4.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "8.0.4",
|
||||
@@ -21,12 +21,14 @@
|
||||
"@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",
|
||||
"isomorphic-git": "1.38.7",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"pino": "9.14.0",
|
||||
"playwright-core": "1.58.2",
|
||||
"plist": "5.0.0",
|
||||
@@ -42,6 +44,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fast-check/vitest": "0.4.1",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@types/node": "24.10.1",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/write-file-atomic": "4.0.3",
|
||||
@@ -52,8 +55,8 @@
|
||||
"jscpd": "5.0.12",
|
||||
"knip": "6.26.0",
|
||||
"lint-staged": "16.4.0",
|
||||
"oxfmt": "0.72.0",
|
||||
"oxlint": "1.73.0",
|
||||
"prettier": "3.6.2",
|
||||
"turbo": "2.10.5",
|
||||
"typedoc": "0.28.20",
|
||||
"typescript": "5.9.3",
|
||||
@@ -1101,6 +1104,329 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@oxfmt/binding-android-arm-eabi": {
|
||||
"version": "0.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.72.0.tgz",
|
||||
"integrity": "sha512-u4U1uaDOVBuorGNC/07NLm48nECHOiJJka002YGML16Md2DRvf38RGBt01lH60EOGvNlFVmbLoksQedNBoZHKw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxfmt/binding-android-arm64": {
|
||||
"version": "0.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.72.0.tgz",
|
||||
"integrity": "sha512-GCnM+Ae2mWOKC+92y3FOb6eZJeVnBAVz82e3lITUUg+kxukYKzBNAvJ/hhHIfa2A0md55SYYhdWPddSSy3KPVA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxfmt/binding-darwin-arm64": {
|
||||
"version": "0.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.72.0.tgz",
|
||||
"integrity": "sha512-LjQ8tlevdtwraFlWC6AnQ1Forzde0clRmnsP3QmK+ydSElDt/J+5Jc+m+//Ly1G97aP7WCNUw110QMIUw2K7xA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxfmt/binding-darwin-x64": {
|
||||
"version": "0.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.72.0.tgz",
|
||||
"integrity": "sha512-+ZbcxhB1C34RQR5IldSyY6Z/yalJIxHXhP36PGE4sEq0f/lgL5jCLmOKoDeVm8dqd9gFGDsd1bNWHSGCAwdSkw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxfmt/binding-freebsd-x64": {
|
||||
"version": "0.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.72.0.tgz",
|
||||
"integrity": "sha512-yGPhA8sePEBFV9iGcprhyOk3Bz3fZshswMH3DpYIPZFLT1DG5LjiZ0j6AJQpSueaL6bUK0av1K+5+fe+ldy5EQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxfmt/binding-linux-arm-gnueabihf": {
|
||||
"version": "0.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.72.0.tgz",
|
||||
"integrity": "sha512-/g5axuqFMcAtoQrFLR5EJEMDjR5Ea4GuzpdDmMpPWAgrF1DmL4kD1NPNVlpXEOxpFJnD6lPvLKYj1lzdlrfifA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxfmt/binding-linux-arm-musleabihf": {
|
||||
"version": "0.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.72.0.tgz",
|
||||
"integrity": "sha512-InVitdHbS09PM+w+WinIiyX37rC9R3L7lUP9mKy9uKTEDSUFaLjWsLoreRIxKivnv7Ndqc6Trvi+K+b6ZzhiJg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxfmt/binding-linux-arm64-gnu": {
|
||||
"version": "0.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.72.0.tgz",
|
||||
"integrity": "sha512-WKjlyRAxpPjXoqeuwFBim5LscgvY+RNEiZ971C9JlRQZONhJTsXDki/oHFYQE+fzzRpzXh3owZvOdT3kNL2g+g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxfmt/binding-linux-arm64-musl": {
|
||||
"version": "0.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.72.0.tgz",
|
||||
"integrity": "sha512-poafnbsPpJhR+5Mbwpf8CscugTyiRAzjTOxRVn8ra3X2jnURs2xL5Du2xl5g2iW85lICMglpmLf8jDjZEbNgzQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxfmt/binding-linux-ppc64-gnu": {
|
||||
"version": "0.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.72.0.tgz",
|
||||
"integrity": "sha512-zNUi0gKRsO7S3IfPzsTbnm88OedPuX32hmZFWZRWHvWJlAoZRwVbeO6Nb3hng4vDgL7qUKp4TGqzPz1mhsycYg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxfmt/binding-linux-riscv64-gnu": {
|
||||
"version": "0.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.72.0.tgz",
|
||||
"integrity": "sha512-qTY7z/iyF/rWI2HWwWMevXqZW5PZUOofUy2afK7v6rseXnpld3yjgyIk6pCux3DqWQ0lB+f5atvLJNBwzLvFsw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxfmt/binding-linux-riscv64-musl": {
|
||||
"version": "0.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.72.0.tgz",
|
||||
"integrity": "sha512-w18qfo8vEMD6EKEHtzTvjc6jH8yCyXIT1Bn7PFWJd0PpZyzH/VQDx4xvjCh0LGz6GRzqiezbisTdG9AoXtpkZQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxfmt/binding-linux-s390x-gnu": {
|
||||
"version": "0.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.72.0.tgz",
|
||||
"integrity": "sha512-grW8HwEir+vK7fCcq1ZgL3NHQCfZ1PidiERE9Hbf+24xGys+DoDaoqTht4ccfn0PcBwbViLkwkda/KNpJu2KRQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxfmt/binding-linux-x64-gnu": {
|
||||
"version": "0.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.72.0.tgz",
|
||||
"integrity": "sha512-wQJRQfWBRIn88Pkr449EUViwRayrYUScg7ty+Ihiem21mQ9BUA6S9e/4RAgyNk9lMag0KBouyKskjq61rOC0WQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxfmt/binding-linux-x64-musl": {
|
||||
"version": "0.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.72.0.tgz",
|
||||
"integrity": "sha512-T1B0uQzafxuGPjjaPrZJ497nxoi1BiI65JfsYc56nNpmev3msfb/iaTmH2kQuNFqa8PKxpMlf3GhWrWBvBPORQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxfmt/binding-openharmony-arm64": {
|
||||
"version": "0.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.72.0.tgz",
|
||||
"integrity": "sha512-OKxxgTurl7+hypVplO8QRAbbQlwEs0j1Y6FZv+ZN2qNbdxQk2muMlzuJNBGTCWwxQ0rkkOD1uK/DW1fvzoUSJA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxfmt/binding-win32-arm64-msvc": {
|
||||
"version": "0.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.72.0.tgz",
|
||||
"integrity": "sha512-L78TKzURqvjuZOCo88ixMbPXf/RP7u31Y0cmjJ/KEmrHmfYhmhEvsDaTYPiyTRne64jUgCbADfY0YeoQk4/Xyw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxfmt/binding-win32-ia32-msvc": {
|
||||
"version": "0.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.72.0.tgz",
|
||||
"integrity": "sha512-gQtFW+Ii8BWgtJTAD17TY/WsdNS5gUfAPov8iI1jeq4r9lRRYImmxPL08UU00/RLaCfBJwh2UgINnDY0iOiZ+A==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxfmt/binding-win32-x64-msvc": {
|
||||
"version": "0.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.72.0.tgz",
|
||||
"integrity": "sha512-uhooIN+DCzWX5uH7mQkSVCsODqsGluGJ4n3ch4+mITtWKpESMDjUJUSDBAVFuYQDwqY2McPDKdKJbM0oZVW/Qw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxlint/binding-android-arm-eabi": {
|
||||
"version": "1.73.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.73.0.tgz",
|
||||
@@ -1894,6 +2220,16 @@
|
||||
"assertion-error": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cross-spawn": {
|
||||
"version": "6.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz",
|
||||
"integrity": "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/deep-eql": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||
@@ -3425,6 +3761,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsonc-parser": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
|
||||
"integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/knip": {
|
||||
"version": "6.26.0",
|
||||
"resolved": "https://registry.npmjs.org/knip/-/knip-6.26.0.tgz",
|
||||
@@ -4200,6 +4542,58 @@
|
||||
"@oxc-resolver/binding-win32-x64-msvc": "11.21.3"
|
||||
}
|
||||
},
|
||||
"node_modules/oxfmt": {
|
||||
"version": "0.72.0",
|
||||
"resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.72.0.tgz",
|
||||
"integrity": "sha512-16OQeL0uhPZbfmT9S9WWM0Nv9+BmVi8Ra1uTYaSKj/qSCs6FB30KOhz9JoNYw6thT+K5bVxsk/tSEOu497i34g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinypool": "2.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"oxfmt": "bin/oxfmt"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/oxc-project"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@oxfmt/binding-android-arm-eabi": "0.72.0",
|
||||
"@oxfmt/binding-android-arm64": "0.72.0",
|
||||
"@oxfmt/binding-darwin-arm64": "0.72.0",
|
||||
"@oxfmt/binding-darwin-x64": "0.72.0",
|
||||
"@oxfmt/binding-freebsd-x64": "0.72.0",
|
||||
"@oxfmt/binding-linux-arm-gnueabihf": "0.72.0",
|
||||
"@oxfmt/binding-linux-arm-musleabihf": "0.72.0",
|
||||
"@oxfmt/binding-linux-arm64-gnu": "0.72.0",
|
||||
"@oxfmt/binding-linux-arm64-musl": "0.72.0",
|
||||
"@oxfmt/binding-linux-ppc64-gnu": "0.72.0",
|
||||
"@oxfmt/binding-linux-riscv64-gnu": "0.72.0",
|
||||
"@oxfmt/binding-linux-riscv64-musl": "0.72.0",
|
||||
"@oxfmt/binding-linux-s390x-gnu": "0.72.0",
|
||||
"@oxfmt/binding-linux-x64-gnu": "0.72.0",
|
||||
"@oxfmt/binding-linux-x64-musl": "0.72.0",
|
||||
"@oxfmt/binding-openharmony-arm64": "0.72.0",
|
||||
"@oxfmt/binding-win32-arm64-msvc": "0.72.0",
|
||||
"@oxfmt/binding-win32-ia32-msvc": "0.72.0",
|
||||
"@oxfmt/binding-win32-x64-msvc": "0.72.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.0.0",
|
||||
"vite-plus": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"svelte": {
|
||||
"optional": true
|
||||
},
|
||||
"vite-plus": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/oxlint": {
|
||||
"version": "1.73.0",
|
||||
"resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.73.0.tgz",
|
||||
@@ -4425,22 +4819,6 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
@@ -4963,6 +5341,16 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tinypool": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.2.0.tgz",
|
||||
"integrity": "sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.0.0 || >=22.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyrainbow": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
|
||||
|
||||
+25
-22
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "rea-agents",
|
||||
"mcpName": "io.github.morluto/rea",
|
||||
"version": "3.2.1",
|
||||
"version": "4.0.0",
|
||||
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
@@ -61,14 +61,15 @@
|
||||
"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",
|
||||
@@ -76,8 +77,8 @@
|
||||
"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",
|
||||
@@ -85,20 +86,18 @@
|
||||
"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",
|
||||
@@ -110,8 +109,7 @@
|
||||
"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",
|
||||
@@ -121,6 +119,8 @@
|
||||
"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: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",
|
||||
@@ -131,7 +131,6 @@
|
||||
"verify:javascript-runtime": "npm run build:cached && node scripts/verify-local-javascript-runtime.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",
|
||||
@@ -149,21 +148,22 @@
|
||||
"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",
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"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",
|
||||
@@ -178,12 +178,14 @@
|
||||
"@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",
|
||||
"isomorphic-git": "1.38.7",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"pino": "9.14.0",
|
||||
"playwright-core": "1.58.2",
|
||||
"plist": "5.0.0",
|
||||
@@ -195,6 +197,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fast-check/vitest": "0.4.1",
|
||||
"@types/cross-spawn": "6.0.6",
|
||||
"@types/node": "24.10.1",
|
||||
"@types/semver": "7.7.1",
|
||||
"@types/write-file-atomic": "4.0.3",
|
||||
@@ -205,8 +208,8 @@
|
||||
"jscpd": "5.0.12",
|
||||
"knip": "6.26.0",
|
||||
"lint-staged": "16.4.0",
|
||||
"oxfmt": "0.72.0",
|
||||
"oxlint": "1.73.0",
|
||||
"prettier": "3.6.2",
|
||||
"turbo": "2.10.5",
|
||||
"typedoc": "0.28.20",
|
||||
"typescript": "5.9.3",
|
||||
|
||||
@@ -117,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,6 +1,6 @@
|
||||
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";
|
||||
@@ -89,7 +89,7 @@ async function sdkToolCatalog() {
|
||||
}
|
||||
}
|
||||
const auxiliaryProviders = [
|
||||
new ArtifactProvider(false, false, CATALOG_PLATFORM),
|
||||
new ArtifactProvider(CATALOG_PLATFORM),
|
||||
new NativeMacOSProvider(undefined, CATALOG_PLATFORM),
|
||||
new ManagedStaticProvider(),
|
||||
].map((provider) => ({
|
||||
@@ -110,7 +110,8 @@ const canonicalizeJson = (value) => {
|
||||
const payloadJson = JSON.stringify(
|
||||
canonicalizeJson({ catalog, auxiliaryProviders }),
|
||||
);
|
||||
const source = await format(
|
||||
const source = await formatGeneratedFile(
|
||||
"src/generatedMcpToolCatalog.ts",
|
||||
`import type { ToolAnnotations } from "@modelcontextprotocol/server";
|
||||
import type {
|
||||
CapabilityDescriptor,
|
||||
@@ -194,7 +195,6 @@ function isRecord(
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
`,
|
||||
{ parser: "typescript" },
|
||||
);
|
||||
|
||||
await ensureGeneratedFile({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -21,9 +21,6 @@ export async function verifyLargeScreenshotE2e(endpoint, origin) {
|
||||
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
||||
REA_LOG_LEVEL: "silent",
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
|
||||
REA_BROWSER_OBSERVE_ENABLED: "true",
|
||||
REA_BROWSER_CDP_ENDPOINTS_JSON: JSON.stringify([endpoint]),
|
||||
REA_BROWSER_ALLOWED_ORIGINS_JSON: JSON.stringify([origin]),
|
||||
};
|
||||
let client;
|
||||
let transport;
|
||||
@@ -39,7 +36,15 @@ export async function verifyLargeScreenshotE2e(endpoint, origin) {
|
||||
);
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[entrypoint, "capture-web-screenshot", endpoint, target.id, "--json"],
|
||||
[
|
||||
entrypoint,
|
||||
"capture-web-screenshot",
|
||||
endpoint,
|
||||
target.id,
|
||||
"--allowed-origins",
|
||||
origin,
|
||||
"--json",
|
||||
],
|
||||
{ env, timeout: 60000, maxBuffer: 72 * 1024 * 1024 },
|
||||
);
|
||||
const cliEvidence = parseEvidence(JSON.parse(stdout));
|
||||
@@ -72,7 +77,6 @@ export async function verifyLargeScreenshotE2e(endpoint, origin) {
|
||||
name: "capture_web_screenshot",
|
||||
arguments: {
|
||||
cdp_endpoint: endpoint,
|
||||
allowed_origins: [origin],
|
||||
target_id: target.id,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -9,7 +9,7 @@ import { parseEvidence } from "../../dist/domain/evidence.js";
|
||||
import { analysisProfileSchema } from "../../dist/domain/analysisProfile.js";
|
||||
import { nativeValueTraceSchema } from "../../dist/domain/nativeValueTrace.js";
|
||||
|
||||
const entrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
|
||||
const defaultEntrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
|
||||
const environment = () => ({
|
||||
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
||||
REA_LOG_LEVEL: "silent",
|
||||
@@ -67,7 +67,12 @@ const verifyGraph = (input, target, procedure, globalAddress) => {
|
||||
};
|
||||
|
||||
/** Verify real Ghidra dependency composition through production CLI and stdio MCP. */
|
||||
export async function verifyNativeValueE2e(target, procedure, globalAddress) {
|
||||
export async function verifyNativeValueE2e(
|
||||
target,
|
||||
procedure,
|
||||
globalAddress,
|
||||
{ entrypoint = defaultEntrypoint } = {},
|
||||
) {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[
|
||||
@@ -142,10 +147,7 @@ export async function verifyNativeValueE2e(target, procedure, globalAddress) {
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
assert.notEqual(result.isError, true, JSON.stringify(result));
|
||||
const mcpEvidence = parseEvidence({
|
||||
...result.structuredContent?.evidence,
|
||||
normalized_result: result.structuredContent?.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);
|
||||
@@ -157,6 +159,66 @@ export async function verifyNativeValueE2e(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: {},
|
||||
@@ -175,6 +237,8 @@ export async function verifyNativeValueE2e(target, procedure, globalAddress) {
|
||||
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,
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -58,7 +58,6 @@ try {
|
||||
if (!target.ok) throw target.error;
|
||||
const observation = await observeNativeUi(target.value, "observe_native_ui", {
|
||||
...scope,
|
||||
observation_approved: true,
|
||||
});
|
||||
if (
|
||||
!observation.ok &&
|
||||
@@ -83,8 +82,6 @@ try {
|
||||
"capture_native_ui_scenario",
|
||||
{
|
||||
...scope,
|
||||
observation_approved: true,
|
||||
actions_approved: true,
|
||||
restore: "leave-as-is",
|
||||
screenshot: false,
|
||||
steps: [
|
||||
@@ -114,8 +111,6 @@ try {
|
||||
String(scope.pid),
|
||||
"--window-id",
|
||||
String(scope.window_id),
|
||||
"--observation-approved",
|
||||
"--actions-approved",
|
||||
"--restore",
|
||||
"leave-as-is",
|
||||
"--steps",
|
||||
@@ -135,14 +130,14 @@ try {
|
||||
evidence.normalized_result?.steps?.length !== 2
|
||||
)
|
||||
throw new Error(
|
||||
`Native UI CLI did not preserve the approved scenario: ${JSON.stringify(evidence.error ?? evidence.normalized_result?.steps?.map(({ outcome, reason }) => ({ outcome, reason })))}`,
|
||||
`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, observation_approved: true },
|
||||
scope,
|
||||
);
|
||||
if (mismatch.ok || !mismatch.error.message.includes("target-mismatch"))
|
||||
throw new Error("Real helper did not reject changed target bytes");
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { chmod, mkdir, symlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { supportedClients } from "../dist/application/SupportedClients.js";
|
||||
|
||||
/** Create an isolated prefix, fake PATH, client configs, and environment. */
|
||||
export async function verifyPackageEnvironment({
|
||||
@@ -48,7 +49,11 @@ export async function verifyPackageEnvironment({
|
||||
chmod(hopper, 0o755),
|
||||
chmod(npx, 0o755),
|
||||
]);
|
||||
const claudeDir = join(home, "Library/Application Support/Claude");
|
||||
const claudeDir = supportedClients(home, process.platform, {}).find(
|
||||
({ name }) => name === "claude_desktop",
|
||||
)?.markerPath;
|
||||
if (claudeDir === undefined)
|
||||
throw new Error("Claude Desktop fixture has no configuration directory");
|
||||
const codexDir = join(home, ".codex");
|
||||
const cursorDir = join(home, ".cursor");
|
||||
await Promise.all([
|
||||
@@ -69,6 +74,12 @@ export async function verifyPackageEnvironment({
|
||||
const environment = {
|
||||
...process.env,
|
||||
HOME: home,
|
||||
USERPROFILE: home,
|
||||
APPDATA: join(home, "AppData", "Roaming"),
|
||||
CLAUDE_CONFIG_DIR: home,
|
||||
CODEX_HOME: codexDir,
|
||||
COPILOT_HOME: join(home, ".copilot"),
|
||||
OPENCODE_CONFIG: undefined,
|
||||
XDG_CONFIG_HOME: join(home, ".config"),
|
||||
PATH: `${fakeBin}:${process.env.PATH ?? ""}`,
|
||||
REA_ANALYSIS_PROVIDER: "auto",
|
||||
|
||||
@@ -115,64 +115,6 @@ const verifyManagedReconstruction = async ({
|
||||
throw new Error("packaged managed reconstruction import CLI failed");
|
||||
};
|
||||
|
||||
const verifyManagedRuntimePlan = async ({
|
||||
cli,
|
||||
managedMembers,
|
||||
managedMethod,
|
||||
workspace,
|
||||
environment,
|
||||
}) => {
|
||||
const managedRuntimePlanInput = {
|
||||
static_members: managedMembers,
|
||||
method: {
|
||||
token: managedMethod.token,
|
||||
signature_sha256: managedMethod.signature.raw_sha256,
|
||||
normalized_il_sha256: managedMethod.body.normalized_il_sha256,
|
||||
},
|
||||
requested_effect: "attach",
|
||||
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,
|
||||
},
|
||||
};
|
||||
const managedRuntimePlan = json(
|
||||
await run(
|
||||
cli,
|
||||
[
|
||||
"plan-managed-runtime-correlation",
|
||||
JSON.stringify(managedRuntimePlanInput),
|
||||
"--json",
|
||||
],
|
||||
{
|
||||
...environment,
|
||||
REA_MANAGED_RUNTIME_ENABLED: "true",
|
||||
REA_MANAGED_RUNTIME_ROOTS_JSON: JSON.stringify([workspace]),
|
||||
REA_MANAGED_RUNTIME_EXECUTABLE_PATH: process.execPath,
|
||||
},
|
||||
),
|
||||
);
|
||||
if (
|
||||
managedRuntimePlan.operation !== "plan_managed_runtime_correlation" ||
|
||||
managedRuntimePlan.provider?.id !== "rea-dotnet-workflows" ||
|
||||
managedRuntimePlan.normalized_result?.executed !== false ||
|
||||
managedRuntimePlan.normalized_result?.authority_model?.capability !==
|
||||
"managed_runtime" ||
|
||||
managedRuntimePlan.normalized_result?.effect_taxonomy?.attaches_process !==
|
||||
true ||
|
||||
managedRuntimePlan.normalized_result?.effect_taxonomy
|
||||
?.invokes_target_code !== false
|
||||
)
|
||||
throw new Error("packaged managed runtime-correlation CLI failed");
|
||||
};
|
||||
|
||||
const verifyManagedBoundaries = async ({ cli, managedPath, environment }) => {
|
||||
const managedBoundaries = json(
|
||||
await run(
|
||||
@@ -332,7 +274,8 @@ const verifyManagedComparison = async ({
|
||||
if (
|
||||
managedComparison.operation !== "compare_managed_members" ||
|
||||
managedComparison.provider?.id !== "rea-dotnet-workflows" ||
|
||||
managedComparison.normalized_result?.algorithm?.name_matching !== "not-used"
|
||||
managedComparison.normalized_result?.algorithm?.name_matching !==
|
||||
"exact-signature-fallback"
|
||||
)
|
||||
throw new Error("packaged managed comparison CLI failed");
|
||||
};
|
||||
@@ -358,13 +301,6 @@ export async function verifyManaged({ cli, workspace, environment }) {
|
||||
managedMethod,
|
||||
environment,
|
||||
});
|
||||
await verifyManagedRuntimePlan({
|
||||
cli,
|
||||
managedMembers,
|
||||
managedMethod,
|
||||
workspace,
|
||||
environment,
|
||||
});
|
||||
const managedBoundaries = await verifyManagedBoundaries({
|
||||
cli,
|
||||
managedPath,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { join } from "node:path";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
@@ -5,6 +6,7 @@ import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { MCP_STARTUP_POLICY } from "../dist/mcpStartupPolicy.js";
|
||||
import { parseEvidence } from "../dist/domain/evidence.js";
|
||||
import * as prompts from "./verify-package-prompts.mjs";
|
||||
import { json, verifyCompleteToolCatalog } from "./lib/verify-package-core.mjs";
|
||||
|
||||
@@ -155,8 +157,77 @@ const verifyMcpEvidenceBundle = async (client, mcpOptions, evidenceRoot) => {
|
||||
throw new Error("packaged MCP evidence import failed");
|
||||
};
|
||||
|
||||
/** Connect to the packaged MCP server and exercise the target-free catalog. */
|
||||
export async function verifyPackageMcp({ cli, environment, evidenceRoot }) {
|
||||
const verifyMcpInlineArtifactEvidence = async (
|
||||
client,
|
||||
mcpOptions,
|
||||
artifactArchive,
|
||||
) => {
|
||||
const opened = await client.callTool(
|
||||
{ name: "open_binary", arguments: { path: artifactArchive } },
|
||||
mcpOptions,
|
||||
);
|
||||
assert.notEqual(opened.isError, true, JSON.stringify(opened));
|
||||
try {
|
||||
const inspected = await client.callTool(
|
||||
{ name: "inspect_artifact", arguments: {} },
|
||||
mcpOptions,
|
||||
);
|
||||
assert.notEqual(inspected.isError, true, JSON.stringify(inspected));
|
||||
const source = parseEvidence(inspected.structuredContent?.evidence);
|
||||
assert.deepEqual(
|
||||
source.normalized_result,
|
||||
inspected.structuredContent?.result,
|
||||
);
|
||||
assert.deepEqual(
|
||||
json(prompts.mcpText(inspected)),
|
||||
inspected.structuredContent,
|
||||
);
|
||||
const compared = await client.callTool(
|
||||
{
|
||||
name: "compare_artifacts",
|
||||
arguments: {
|
||||
left: inspected.structuredContent.evidence,
|
||||
right: inspected.structuredContent.evidence,
|
||||
},
|
||||
},
|
||||
mcpOptions,
|
||||
);
|
||||
assert.notEqual(compared.isError, true, JSON.stringify(compared));
|
||||
const comparison = parseEvidence(compared.structuredContent?.evidence);
|
||||
assert.deepEqual(
|
||||
comparison.normalized_result,
|
||||
compared.structuredContent?.result,
|
||||
);
|
||||
assert.ok(comparison.evidence_links.includes(source.evidence_id));
|
||||
const bundle = await client.callTool(
|
||||
{ name: "get_evidence_bundle", arguments: {} },
|
||||
mcpOptions,
|
||||
);
|
||||
assert.notEqual(bundle.isError, true, JSON.stringify(bundle));
|
||||
for (const evidence of [source, comparison]) {
|
||||
assert.deepEqual(
|
||||
bundle.structuredContent?.result?.records.find(
|
||||
(record) => record.evidence_id === evidence.evidence_id,
|
||||
),
|
||||
evidence,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
const closed = await client.callTool(
|
||||
{ name: "close_binary", arguments: {} },
|
||||
mcpOptions,
|
||||
);
|
||||
assert.notEqual(closed.isError, true, JSON.stringify(closed));
|
||||
}
|
||||
};
|
||||
|
||||
/** Connect to the packaged MCP server and verify catalog and Evidence composition. */
|
||||
export async function verifyPackageMcp({
|
||||
cli,
|
||||
environment,
|
||||
evidenceRoot,
|
||||
artifactArchive,
|
||||
}) {
|
||||
const diagnosed = json(
|
||||
(
|
||||
await execute(cli, ["mcp", "doctor", "--json"], {
|
||||
@@ -191,6 +262,7 @@ export async function verifyPackageMcp({ cli, environment, evidenceRoot }) {
|
||||
await verifyMcpTargetFree(client, mcpOptions);
|
||||
await verifyMcpUnknownProvider(client, mcpOptions);
|
||||
await verifyMcpBinaryLifecycle(client, mcpOptions);
|
||||
await verifyMcpInlineArtifactEvidence(client, mcpOptions, artifactArchive);
|
||||
await verifyMcpEvidenceBundle(client, mcpOptions, evidenceRoot);
|
||||
} catch (cause) {
|
||||
throw new Error(`packaged MCP smoke failed: ${mcpStderr}`, { cause });
|
||||
|
||||
@@ -18,6 +18,16 @@ import {
|
||||
run,
|
||||
runWithStatus,
|
||||
} from "./lib/verify-package-core.mjs";
|
||||
import { parse as parseJsonc } from "jsonc-parser";
|
||||
|
||||
const OPENCODE_ORIGINAL = `{
|
||||
// OpenCode preferences must survive setup.
|
||||
"model": "provider/model",
|
||||
"mcp": {
|
||||
// Keep this independently managed server.
|
||||
"other": { "type": "local", "command": ["node", "other.js"] },
|
||||
},
|
||||
}\n`;
|
||||
|
||||
const verifyMcpAdd = async ({ cli, environment, npxLog }) => {
|
||||
await run(cli, ["mcp", "add"], environment);
|
||||
@@ -27,12 +37,48 @@ const verifyMcpAdd = async ({ cli, environment, npxLog }) => {
|
||||
throw new Error("Incur mcp add did not register the pinned npx command");
|
||||
};
|
||||
|
||||
const verifyInteractiveSetup = async ({ command, environment, root }) =>
|
||||
const snapshotFiles = async (paths) =>
|
||||
Promise.all(
|
||||
paths.map(async (path) => {
|
||||
const exists = await pathExists(path);
|
||||
return {
|
||||
path,
|
||||
exists,
|
||||
content: exists ? await readFile(path, "utf8") : null,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const assertFilesUnchanged = async (snapshot, label) => {
|
||||
for (const entry of snapshot) {
|
||||
const exists = await pathExists(entry.path);
|
||||
const content = exists ? await readFile(entry.path, "utf8") : null;
|
||||
if (exists !== entry.exists || content !== entry.content)
|
||||
throw new Error(`${label} changed ${entry.path}`);
|
||||
}
|
||||
};
|
||||
|
||||
const verifyInteractiveSetup = async ({
|
||||
command,
|
||||
environment,
|
||||
root,
|
||||
unchangedPaths,
|
||||
}) =>
|
||||
new Promise((resolvePromise, reject) => {
|
||||
let output = "";
|
||||
let cancelled = false;
|
||||
let settled = false;
|
||||
const terminal = spawn(command, ["setup"], {
|
||||
const unchanged = snapshotFiles(unchangedPaths);
|
||||
const wrapper = `
|
||||
const { spawnSync } = require("node:child_process");
|
||||
const child = spawnSync(process.argv[1], ["setup"], { stdio: "inherit", env: process.env, shell: process.platform === "win32" });
|
||||
const terminal = process.platform === "win32"
|
||||
? spawnSync("cmd.exe", ["/c", "mode", "CON"], { encoding: "utf8", stdio: ["inherit", "pipe", "inherit"] })
|
||||
: spawnSync("stty", ["-a"], { encoding: "utf8", stdio: ["inherit", "pipe", "inherit"] });
|
||||
process.stdout.write("\\n__REA_SETUP_EXIT__" + JSON.stringify({ status: child.status, termios: terminal.stdout, error: child.error?.message }));
|
||||
process.exit(child.status ?? 1);
|
||||
`;
|
||||
const terminal = spawn(process.execPath, ["-e", wrapper, command], {
|
||||
cwd: root,
|
||||
env: { ...environment, NO_COLOR: "1", TERM: "xterm-256color" },
|
||||
name: "xterm-256color",
|
||||
@@ -55,21 +101,120 @@ const verifyInteractiveSetup = async ({ command, environment, root }) =>
|
||||
|
||||
terminal.onData((data) => {
|
||||
output += data;
|
||||
if (!cancelled && output.includes("What should REA set up?")) {
|
||||
if (!cancelled && output.includes("Which agents should use REA?")) {
|
||||
cancelled = true;
|
||||
terminal.write("\u0003");
|
||||
}
|
||||
});
|
||||
terminal.onExit(() => {
|
||||
if (!cancelled || !output.includes("No changes were made.")) {
|
||||
terminal.onExit(async ({ exitCode }) => {
|
||||
const marker = output.lastIndexOf("__REA_SETUP_EXIT__");
|
||||
const result = marker < 0 ? undefined : output.slice(marker + 18).trim();
|
||||
let childResult;
|
||||
try {
|
||||
childResult = result === undefined ? undefined : JSON.parse(result);
|
||||
} catch {
|
||||
childResult = undefined;
|
||||
}
|
||||
if (
|
||||
!cancelled ||
|
||||
exitCode !== 0 ||
|
||||
childResult?.status !== 0 ||
|
||||
!output.includes("\u001b[?25h") ||
|
||||
(process.platform !== "win32" &&
|
||||
(!childResult.termios?.includes("icanon") ||
|
||||
!childResult.termios?.includes("echo"))) ||
|
||||
!output.includes("No changes were made.")
|
||||
) {
|
||||
finish(
|
||||
new Error(
|
||||
`packaged rea-agents setup did not open and cancel the wizard: ${output}`,
|
||||
`packaged rea-agents setup did not cancel with a restored terminal and exit 0: ${output}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
finish();
|
||||
try {
|
||||
await assertFilesUnchanged(await unchanged, "Interactive cancellation");
|
||||
finish();
|
||||
} catch (error) {
|
||||
finish(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const verifyRedirectedStdoutSetup = async ({
|
||||
command,
|
||||
environment,
|
||||
root,
|
||||
unchangedPaths,
|
||||
}) =>
|
||||
new Promise((resolvePromise, reject) => {
|
||||
let output = "";
|
||||
let settled = false;
|
||||
const unchanged = snapshotFiles(unchangedPaths);
|
||||
const wrapper = `
|
||||
const { spawnSync } = require("node:child_process");
|
||||
const child = spawnSync(process.argv[1], ["setup"], { encoding: "utf8", env: process.env, stdio: ["inherit", "pipe", "inherit"], shell: process.platform === "win32" });
|
||||
process.stdout.write("\\n__REA_REDIRECTED_SETUP__" + JSON.stringify({ status: child.status, stdout: child.stdout, error: child.error?.message, stdinTTY: process.stdin.isTTY, stderrTTY: process.stderr.isTTY, stdoutRedirected: true }));
|
||||
process.exit(child.status ?? 1);
|
||||
`;
|
||||
const terminal = spawn(process.execPath, ["-e", wrapper, command], {
|
||||
cwd: root,
|
||||
env: { ...environment, NO_COLOR: "1", TERM: "xterm-256color" },
|
||||
name: "xterm-256color",
|
||||
cols: 120,
|
||||
rows: 40,
|
||||
});
|
||||
const finish = (error) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timeout);
|
||||
if (error === undefined) resolvePromise();
|
||||
else reject(error);
|
||||
};
|
||||
const timeout = setTimeout(() => {
|
||||
terminal.kill();
|
||||
finish(
|
||||
new Error(
|
||||
`packaged setup started an interactive prompt with redirected stdout: ${output}`,
|
||||
),
|
||||
);
|
||||
}, 15_000);
|
||||
terminal.onData((data) => {
|
||||
output += data;
|
||||
});
|
||||
terminal.onExit(async ({ exitCode }) => {
|
||||
const marker = output.lastIndexOf("__REA_REDIRECTED_SETUP__");
|
||||
let childResult;
|
||||
try {
|
||||
childResult =
|
||||
marker < 0 ? undefined : JSON.parse(output.slice(marker + 24).trim());
|
||||
} catch {
|
||||
childResult = undefined;
|
||||
}
|
||||
const allOutput = `${output}\n${childResult?.stdout ?? ""}`;
|
||||
if (
|
||||
(exitCode !== 0 && exitCode !== 1) ||
|
||||
childResult?.status !== exitCode ||
|
||||
childResult.stdinTTY !== true ||
|
||||
childResult.stderrTTY !== true ||
|
||||
childResult.stdoutRedirected !== true ||
|
||||
childResult.stdout?.includes("Which agents should use REA?") ||
|
||||
allOutput.includes("Which agents should use REA?") ||
|
||||
/\u001b\[[0-9;?]*[ -/]*[@-~]/u.test(allOutput)
|
||||
) {
|
||||
finish(
|
||||
new Error(
|
||||
`packaged setup did not remain plain and non-interactive with redirected stdout: ${output}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await assertFilesUnchanged(await unchanged, "Redirected-stdout setup");
|
||||
finish();
|
||||
} catch (error) {
|
||||
finish(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,11 +224,13 @@ const verifySetupPlan = async ({
|
||||
claudeConfig,
|
||||
cursorConfig,
|
||||
codexTarget,
|
||||
opencodeConfig,
|
||||
opencodeOriginal,
|
||||
supportedSetupHost,
|
||||
}) => {
|
||||
const plannedExecution = await runWithStatus(
|
||||
cli,
|
||||
["setup", "--json"],
|
||||
["setup", "--all-detected", "--dry-run", "--json"],
|
||||
environment,
|
||||
);
|
||||
const planned = json(plannedExecution.stdout);
|
||||
@@ -91,15 +238,17 @@ const verifySetupPlan = async ({
|
||||
const plannedClaudeConfig = await readFile(claudeConfig, "utf8");
|
||||
const plannedCodexConfig = await readFile(codexTarget, "utf8");
|
||||
const plannedCursorConfig = await readFile(cursorConfig, "utf8");
|
||||
const plannedOpenCodeConfig = await readFile(opencodeConfig, "utf8");
|
||||
if (
|
||||
planned.status !== "needs_confirmation" ||
|
||||
plannedExecution.status !== 1 ||
|
||||
planned.status !== "planned" ||
|
||||
plannedExecution.status !== 0 ||
|
||||
planned.appliedActions.length !== 0 ||
|
||||
!planned.plannedActions.some(({ kind }) => kind === "configure_client") ||
|
||||
!planned.plannedActions.some(({ kind }) => kind === "install_skill") ||
|
||||
plannedClaudeConfig !== '{"existing":true}\n' ||
|
||||
plannedCodexConfig !== 'model = "gpt-5"\n' ||
|
||||
plannedCursorConfig !== '{"existing":true}\n'
|
||||
plannedCursorConfig !== '{"existing":true}\n' ||
|
||||
plannedOpenCodeConfig !== opencodeOriginal
|
||||
)
|
||||
throw new Error(
|
||||
`packaged setup plan was not read-only: ${JSON.stringify({ status: planned.status, exitCode: plannedExecution.status, plannedKinds: planned.plannedActions.map(({ kind }) => kind), appliedKinds: planned.appliedActions.map(({ kind }) => kind), claudeConfig: plannedClaudeConfig, codexConfig: plannedCodexConfig, cursorConfig: plannedCursorConfig })}`,
|
||||
@@ -108,11 +257,101 @@ const verifySetupPlan = async ({
|
||||
return { planned };
|
||||
};
|
||||
|
||||
const verifySetupApply = async ({ cli, environment, supportedSetupHost }) => {
|
||||
const status = process.platform === "linux" ? "needs_human" : "ready";
|
||||
const verifyAbsentOpenCodeSetup = async ({ cli, environment, home }) => {
|
||||
const xdgConfigHome = join(home, ".xdg-opencode-absent");
|
||||
const configPath = join(xdgConfigHome, "opencode", "opencode.json");
|
||||
if (await pathExists(configPath))
|
||||
throw new Error("packaged OpenCode first-run fixture was not absent");
|
||||
const execution = await runWithStatus(
|
||||
cli,
|
||||
["setup", "--yes", "--client", "opencode", "--skill=false", "--json"],
|
||||
{ ...environment, XDG_CONFIG_HOME: xdgConfigHome },
|
||||
);
|
||||
const result = json(execution.stdout);
|
||||
const config = JSON.parse(await readFile(configPath, "utf8"));
|
||||
if (
|
||||
execution.status !== 0 ||
|
||||
result.status !== "ready" ||
|
||||
!result.appliedActions.includes("configured_opencode") ||
|
||||
config.mcp?.rea?.type !== "local" ||
|
||||
config.mcp.rea.enabled !== true ||
|
||||
JSON.stringify(config.mcp.rea.command) !== JSON.stringify([cli, "mcp"])
|
||||
)
|
||||
throw new Error(
|
||||
`packaged setup did not configure explicitly selected OpenCode without a config file: ${JSON.stringify(result)}`,
|
||||
);
|
||||
await rm(xdgConfigHome, { recursive: true, force: true });
|
||||
};
|
||||
|
||||
const verifyAbsentClaudeCodeSetup = async ({ cli, environment, home }) => {
|
||||
const freshHome = join(home, ".claude-code-absent-home");
|
||||
const configPath = join(freshHome, ".claude.json");
|
||||
const markerPath = join(freshHome, ".claude");
|
||||
await rm(freshHome, { recursive: true, force: true });
|
||||
await mkdir(freshHome, { recursive: true });
|
||||
const childEnvironment = {
|
||||
...environment,
|
||||
HOME: freshHome,
|
||||
USERPROFILE: freshHome,
|
||||
XDG_CONFIG_HOME: join(freshHome, ".config"),
|
||||
};
|
||||
delete childEnvironment.CLAUDE_CONFIG_DIR;
|
||||
if ((await pathExists(configPath)) || (await pathExists(markerPath)))
|
||||
throw new Error("packaged Claude Code first-run fixture was not absent");
|
||||
const args = [
|
||||
"setup",
|
||||
"--yes",
|
||||
"--client",
|
||||
"claude_code",
|
||||
"--skill=false",
|
||||
"--json",
|
||||
];
|
||||
const firstExecution = await runWithStatus(cli, args, childEnvironment);
|
||||
const first = json(firstExecution.stdout);
|
||||
const config = json(await readFile(configPath, "utf8"));
|
||||
const registration = first.doctor.identity?.registrations?.find(
|
||||
({ client }) => client === "claude_code",
|
||||
);
|
||||
if (
|
||||
firstExecution.status !== 0 ||
|
||||
first.status !== "ready" ||
|
||||
!first.appliedActions.includes("configured_claude_code") ||
|
||||
config.mcpServers?.rea?.command !== cli ||
|
||||
JSON.stringify(config.mcpServers?.rea?.args) !== JSON.stringify(["mcp"]) ||
|
||||
(await pathExists(markerPath)) ||
|
||||
registration?.state !== "aligned" ||
|
||||
registration.config_path !== configPath
|
||||
)
|
||||
throw new Error(
|
||||
`packaged setup did not configure an explicitly selected Claude Code with no marker directory: ${JSON.stringify(first)}`,
|
||||
);
|
||||
const repeatExecution = await runWithStatus(cli, args, childEnvironment);
|
||||
const repeat = json(repeatExecution.stdout);
|
||||
if (
|
||||
repeatExecution.status !== 0 ||
|
||||
repeat.status !== "ready" ||
|
||||
repeat.appliedActions.length !== 0 ||
|
||||
repeat.plannedActions.length !== 0
|
||||
)
|
||||
throw new Error(
|
||||
`packaged setup was not idempotent for Claude Code without a marker directory: ${JSON.stringify(repeat)}`,
|
||||
);
|
||||
await rm(freshHome, { recursive: true, force: true });
|
||||
};
|
||||
|
||||
const verifySetupApply = async ({
|
||||
cli,
|
||||
environment,
|
||||
supportedSetupHost,
|
||||
hopperSetupSupported,
|
||||
}) => {
|
||||
const status = "ready";
|
||||
const hopperStatus = hopperSetupSupported
|
||||
? "needs_confirmation"
|
||||
: "needs_human";
|
||||
const firstExecution = await runWithStatus(
|
||||
cli,
|
||||
["setup", "--yes", "--json"],
|
||||
["setup", "--yes", "--all-detected", "--json"],
|
||||
environment,
|
||||
);
|
||||
const first = json(firstExecution.stdout);
|
||||
@@ -124,14 +363,24 @@ const verifySetupApply = async ({ cli, environment, supportedSetupHost }) => {
|
||||
const aligned = json(alignedExecution.stdout);
|
||||
const secondExecution = await runWithStatus(
|
||||
cli,
|
||||
["setup", "--install-hopper", "--json"],
|
||||
[
|
||||
"setup",
|
||||
"--client",
|
||||
"claude_desktop",
|
||||
"--client",
|
||||
"codex",
|
||||
"--client",
|
||||
"cursor",
|
||||
"--install-hopper",
|
||||
"--json",
|
||||
],
|
||||
environment,
|
||||
);
|
||||
const second = json(secondExecution.stdout);
|
||||
if (supportedSetupHost) {
|
||||
if (
|
||||
first.status !== status ||
|
||||
second.status !== "needs_confirmation" ||
|
||||
second.status !== hopperStatus ||
|
||||
firstExecution.status !== (status === "ready" ? 0 : 1) ||
|
||||
secondExecution.status !== 1 ||
|
||||
aligned.status !== status ||
|
||||
@@ -170,6 +419,31 @@ const assertClientConfig = async (cli, configPath) => {
|
||||
throw new Error("packaged client backup failed");
|
||||
};
|
||||
|
||||
const assertOpenCodeConfig = async ({
|
||||
cli,
|
||||
configPath,
|
||||
expectedRegistration,
|
||||
}) => {
|
||||
const contents = await readFile(configPath, "utf8");
|
||||
const errors = [];
|
||||
const parsed = parseJsonc(contents, errors, { allowTrailingComma: true });
|
||||
const config = parsed;
|
||||
const mcp = config?.mcp;
|
||||
if (
|
||||
errors.length !== 0 ||
|
||||
!contents.includes("// OpenCode preferences must survive setup.") ||
|
||||
!contents.includes("// Keep this independently managed server.") ||
|
||||
config?.model !== "provider/model" ||
|
||||
mcp?.other?.command?.[0] !== "node" ||
|
||||
(expectedRegistration &&
|
||||
(mcp?.rea?.type !== "local" ||
|
||||
mcp.rea.enabled !== true ||
|
||||
JSON.stringify(mcp.rea.command) !== JSON.stringify([cli, "mcp"]))) ||
|
||||
(!expectedRegistration && mcp?.rea !== undefined)
|
||||
)
|
||||
throw new Error("packaged OpenCode JSONC registration readback failed");
|
||||
};
|
||||
|
||||
const assertCodexSymlink = async ({
|
||||
cli,
|
||||
codexConfig,
|
||||
@@ -245,6 +519,8 @@ const verifyConfigReadback = async ({
|
||||
root,
|
||||
skillPath,
|
||||
siblingSkillPath,
|
||||
opencodeConfig,
|
||||
opencodeOriginal,
|
||||
}) => {
|
||||
await assertClientConfig(cli, claudeConfig);
|
||||
await assertClientConfig(cli, cursorConfig);
|
||||
@@ -255,6 +531,18 @@ const verifyConfigReadback = async ({
|
||||
codexTarget,
|
||||
});
|
||||
await assertSkill({ skillPath, siblingSkillPath, root });
|
||||
await assertOpenCodeConfig({
|
||||
cli,
|
||||
configPath: opencodeConfig,
|
||||
expectedRegistration: true,
|
||||
});
|
||||
if (
|
||||
(await readFile(`${opencodeConfig}.rea.backup`, "utf8")) !==
|
||||
opencodeOriginal
|
||||
)
|
||||
throw new Error(
|
||||
"packaged OpenCode backup did not preserve the original JSONC",
|
||||
);
|
||||
await assertAlignedDoctor(cli, environment);
|
||||
};
|
||||
|
||||
@@ -269,7 +557,7 @@ const verifySetupFailureRecovery = async ({
|
||||
await writeFile(cursorConfig, '{"existing":true}\n');
|
||||
const failedExecution = await runWithStatus(
|
||||
cli,
|
||||
["setup", "--yes", "--json"],
|
||||
["setup", "--yes", "--all-detected", "--json"],
|
||||
environment,
|
||||
);
|
||||
const failed = json(failedExecution.stdout);
|
||||
@@ -289,7 +577,7 @@ const verifySetupFailureRecovery = async ({
|
||||
await writeFile(claudeConfig, "{}\n");
|
||||
const recoveredExecution = await runWithStatus(
|
||||
cli,
|
||||
["setup", "--yes", "--json"],
|
||||
["setup", "--yes", "--all-detected", "--json"],
|
||||
environment,
|
||||
);
|
||||
const recovered = json(recoveredExecution.stdout);
|
||||
@@ -308,7 +596,7 @@ const verifyDanglingSymlink = async ({ cli, environment, home }) => {
|
||||
await symlink(join(home, "missing-gemini.json"), geminiConfig);
|
||||
const danglingExecution = await runWithStatus(
|
||||
cli,
|
||||
["setup", "--yes", "--json"],
|
||||
["setup", "--yes", "--client", "gemini_cli", "--json"],
|
||||
environment,
|
||||
);
|
||||
const dangling = json(danglingExecution.stdout);
|
||||
@@ -335,7 +623,9 @@ const verifyUninstall = async ({
|
||||
codexConfig,
|
||||
cursorTarget,
|
||||
codexTarget,
|
||||
opencodeConfig,
|
||||
}) => {
|
||||
const openCodeBeforeUninstall = await readFile(opencodeConfig, "utf8");
|
||||
const uninstallExecution = await runWithStatus(
|
||||
cli,
|
||||
["uninstall", "--json"],
|
||||
@@ -344,6 +634,12 @@ const verifyUninstall = async ({
|
||||
const uninstall = json(uninstallExecution.stdout);
|
||||
const cursorAfterUninstall = json(await readFile(cursorTarget, "utf8"));
|
||||
const codexAfterUninstall = await readFile(codexTarget, "utf8");
|
||||
const openCodeErrors = [];
|
||||
const openCodeAfterUninstall = parseJsonc(
|
||||
await readFile(opencodeConfig, "utf8"),
|
||||
openCodeErrors,
|
||||
{ allowTrailingComma: true },
|
||||
);
|
||||
if (
|
||||
uninstall.status !== "complete" ||
|
||||
uninstallExecution.status !== 0 ||
|
||||
@@ -357,6 +653,18 @@ const verifyUninstall = async ({
|
||||
cursorAfterUninstall.mcpServers?.rea !== undefined ||
|
||||
!codexAfterUninstall.includes('model = "gpt-5"') ||
|
||||
codexAfterUninstall.includes("mcp_servers.rea") ||
|
||||
openCodeErrors.length !== 0 ||
|
||||
openCodeAfterUninstall?.model !== "provider/model" ||
|
||||
openCodeAfterUninstall?.mcp?.rea !== undefined ||
|
||||
openCodeAfterUninstall?.mcp?.other?.command?.[0] !== "node" ||
|
||||
!(await readFile(`${opencodeConfig}.rea.backup`, "utf8")).includes(
|
||||
"OpenCode preferences must survive setup",
|
||||
) ||
|
||||
!(await readFile(`${opencodeConfig}.rea.backup`, "utf8")).includes(
|
||||
"Keep this independently managed server",
|
||||
) ||
|
||||
(await readFile(`${opencodeConfig}.rea.backup`, "utf8")) !==
|
||||
openCodeBeforeUninstall ||
|
||||
!(await readFile(`${cursorConfig}.rea.backup`, "utf8")).includes('"rea"') ||
|
||||
!(await readFile(`${codexConfig}.rea.backup`, "utf8")).includes(
|
||||
"[mcp_servers.rea]",
|
||||
@@ -380,13 +688,9 @@ export async function verifyPackageSetup({
|
||||
codexTarget,
|
||||
cursorTarget,
|
||||
supportedSetupHost,
|
||||
hopperSetupSupported,
|
||||
root,
|
||||
}) {
|
||||
await verifyInteractiveSetup({
|
||||
command: packageRunnerCli,
|
||||
environment,
|
||||
root,
|
||||
});
|
||||
await verifyMcpAdd({ cli, environment, npxLog });
|
||||
const skillPath = join(
|
||||
home,
|
||||
@@ -397,20 +701,53 @@ export async function verifyPackageSetup({
|
||||
await mkdir(join(home, ".agents/skills/unrelated"), { recursive: true });
|
||||
await writeFile(siblingSkillPath, "unrelated skill\n");
|
||||
}
|
||||
const opencodeConfig = join(
|
||||
environment.XDG_CONFIG_HOME ?? join(home, ".config"),
|
||||
"opencode",
|
||||
"opencode.json",
|
||||
);
|
||||
if (supportedSetupHost) {
|
||||
await mkdir(join(opencodeConfig, ".."), { recursive: true });
|
||||
await writeFile(opencodeConfig, OPENCODE_ORIGINAL);
|
||||
}
|
||||
const unchangedPaths = [
|
||||
claudeConfig,
|
||||
cursorConfig,
|
||||
codexTarget,
|
||||
opencodeConfig,
|
||||
skillPath,
|
||||
];
|
||||
await verifyInteractiveSetup({
|
||||
command: packageRunnerCli,
|
||||
environment,
|
||||
root,
|
||||
unchangedPaths,
|
||||
});
|
||||
await verifyRedirectedStdoutSetup({
|
||||
command: packageRunnerCli,
|
||||
environment,
|
||||
root,
|
||||
unchangedPaths,
|
||||
});
|
||||
await verifySetupPlan({
|
||||
cli,
|
||||
environment,
|
||||
claudeConfig,
|
||||
cursorConfig,
|
||||
codexTarget,
|
||||
opencodeConfig,
|
||||
opencodeOriginal: OPENCODE_ORIGINAL,
|
||||
supportedSetupHost,
|
||||
});
|
||||
const apply = await verifySetupApply({
|
||||
cli,
|
||||
environment,
|
||||
supportedSetupHost,
|
||||
hopperSetupSupported,
|
||||
});
|
||||
if (supportedSetupHost) {
|
||||
await verifyAbsentOpenCodeSetup({ cli, environment, home });
|
||||
await verifyAbsentClaudeCodeSetup({ cli, environment, home });
|
||||
await verifyConfigReadback({
|
||||
cli,
|
||||
environment,
|
||||
@@ -421,6 +758,8 @@ export async function verifyPackageSetup({
|
||||
root,
|
||||
skillPath,
|
||||
siblingSkillPath,
|
||||
opencodeConfig,
|
||||
opencodeOriginal: OPENCODE_ORIGINAL,
|
||||
});
|
||||
await verifySetupFailureRecovery({
|
||||
cli,
|
||||
@@ -437,6 +776,7 @@ export async function verifyPackageSetup({
|
||||
codexConfig,
|
||||
cursorTarget,
|
||||
codexTarget,
|
||||
opencodeConfig,
|
||||
});
|
||||
} else if (
|
||||
apply.first.status !== "needs_human" ||
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { createServer } from "node:http";
|
||||
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
import { exec, json } from "./lib/verify-package-core.mjs";
|
||||
|
||||
/** Exercise the packaged updater with real npm and a local release registry fixture. */
|
||||
export async function verifyPackageUpdate({
|
||||
prefix,
|
||||
packageRoot,
|
||||
tarball,
|
||||
workspace,
|
||||
environment,
|
||||
}) {
|
||||
const metadataPath = join(packageRoot, "package.json");
|
||||
const metadata = json(await readFile(metadataPath, "utf8"));
|
||||
const archive = await readFile(tarball);
|
||||
const entry = join(packageRoot, "scripts", "rea.mjs");
|
||||
const home = join(workspace, "update-home");
|
||||
const codexPath = join(home, ".codex", "config.toml");
|
||||
const originalConfig =
|
||||
'[mcp_servers.rea]\ncommand = "npx"\nargs = ["-y", "rea-agents@0.0.0", "mcp"]\n';
|
||||
await mkdir(dirname(codexPath), { recursive: true });
|
||||
await writeFile(codexPath, originalConfig);
|
||||
const server = createServer((request, response) => {
|
||||
void serveRegistry(request, response).catch((error) => {
|
||||
response.writeHead(500);
|
||||
response.end(String(error));
|
||||
});
|
||||
});
|
||||
let registry;
|
||||
async function serveRegistry(request, response) {
|
||||
const pathname = new URL(request.url, registry).pathname;
|
||||
if (pathname === "/rea-agents") {
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
name: metadata.name,
|
||||
"dist-tags": { latest: metadata.version },
|
||||
versions: {
|
||||
[metadata.version]: {
|
||||
...metadata,
|
||||
dist: {
|
||||
tarball: `${registry}/release.tgz`,
|
||||
integrity: `sha512-${createHash("sha512").update(archive).digest("base64")}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else if (pathname === "/release.tgz") {
|
||||
response.end(archive);
|
||||
} else if (request.method === "POST") {
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
response.end("{}");
|
||||
} else {
|
||||
const upstream = await fetch(`https://registry.npmjs.org${request.url}`);
|
||||
response.writeHead(upstream.status, {
|
||||
"Content-Type":
|
||||
upstream.headers.get("content-type") ?? "application/octet-stream",
|
||||
});
|
||||
response.end(Buffer.from(await upstream.arrayBuffer()));
|
||||
}
|
||||
}
|
||||
await new Promise((resolveListen, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", resolveListen);
|
||||
});
|
||||
try {
|
||||
const address = server.address();
|
||||
if (typeof address !== "object" || address === null)
|
||||
throw new Error("Registry fixture did not bind");
|
||||
registry = `http://127.0.0.1:${address.port}`;
|
||||
// Model an older installed release's identity while retaining the new updater
|
||||
// under test. The registry supplies the unmodified packed target artifact.
|
||||
const generatedPath = join(
|
||||
packageRoot,
|
||||
"dist",
|
||||
"generatedPackageMetadata.js",
|
||||
);
|
||||
const generated = await readFile(generatedPath, "utf8");
|
||||
const previousVersion = "0.0.0";
|
||||
const previousGenerated = generated.replace(
|
||||
`version: "${metadata.version}"`,
|
||||
`version: "${previousVersion}"`,
|
||||
);
|
||||
if (previousGenerated === generated)
|
||||
throw new Error("Could not create previous-release identity fixture");
|
||||
await writeFile(generatedPath, previousGenerated);
|
||||
await writeFile(
|
||||
metadataPath,
|
||||
JSON.stringify({ ...metadata, version: previousVersion }),
|
||||
);
|
||||
const runtimeEnvironment = {
|
||||
...environment,
|
||||
HOME: home,
|
||||
USERPROFILE: home,
|
||||
APPDATA: join(home, "AppData", "Roaming"),
|
||||
CLAUDE_CONFIG_DIR: home,
|
||||
CODEX_HOME: join(home, ".codex"),
|
||||
COPILOT_HOME: join(home, ".copilot"),
|
||||
XDG_CONFIG_HOME: join(home, ".config"),
|
||||
OPENCODE_CONFIG: join(home, ".config", "opencode", "opencode.json"),
|
||||
npm_config_prefix: prefix,
|
||||
npm_config_registry: registry,
|
||||
};
|
||||
const previous = (
|
||||
await exec(process.execPath, [entry, "--version"], {
|
||||
env: runtimeEnvironment,
|
||||
})
|
||||
).stdout.trim();
|
||||
if (previous !== previousVersion)
|
||||
throw new Error(`Previous-release fixture reported ${previous}`);
|
||||
const harness = join(workspace, "verify-update.mjs");
|
||||
await writeFile(
|
||||
harness,
|
||||
`
|
||||
import { runUpdate } from ${JSON.stringify(pathToFileURL(join(packageRoot, "dist/application/Update.js")).href)};
|
||||
import { systemUpdateHost } from ${JSON.stringify(pathToFileURL(join(packageRoot, "dist/application/UpdateRuntime.js")).href)};
|
||||
const host = systemUpdateHost(${JSON.stringify(packageRoot)}, ${JSON.stringify(home)});
|
||||
const result = await runUpdate(${JSON.stringify(previousVersion)}, host, "structured");
|
||||
process.stdout.write(JSON.stringify(result));
|
||||
if (result.status === "failed") process.exitCode = 1;
|
||||
`,
|
||||
);
|
||||
const result = json(
|
||||
(
|
||||
await exec(process.execPath, [harness], {
|
||||
env: runtimeEnvironment,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
})
|
||||
).stdout,
|
||||
);
|
||||
if (
|
||||
result.status !== "updated" ||
|
||||
result.installedVersion !== metadata.version ||
|
||||
result.command.at(-1) !== `${metadata.name}@${metadata.version}` ||
|
||||
result.maintenance.status !== "planned" ||
|
||||
result.maintenance.scope.clients.join(",") !== "codex" ||
|
||||
result.maintenance.scope.skill !== false
|
||||
)
|
||||
throw new Error(
|
||||
`Packaged updater failed its exact-release contract: ${JSON.stringify(result)}`,
|
||||
);
|
||||
if ((await readFile(codexPath, "utf8")) !== originalConfig)
|
||||
throw new Error(
|
||||
"Update applied integration maintenance without approval",
|
||||
);
|
||||
const current = (
|
||||
await exec(process.execPath, [entry, "--version"], {
|
||||
env: runtimeEnvironment,
|
||||
})
|
||||
).stdout.trim();
|
||||
if (current !== metadata.version)
|
||||
throw new Error(`Updated executable reported ${current}`);
|
||||
const help = (
|
||||
await exec(process.execPath, [entry, "--help"], {
|
||||
env: runtimeEnvironment,
|
||||
})
|
||||
).stdout;
|
||||
if (!/^\s{2}update\s/mu.test(help) || /^\s{2}upgrade\s/mu.test(help))
|
||||
throw new Error(
|
||||
"Updated CLI did not expose update as its sole maintenance command",
|
||||
);
|
||||
return {
|
||||
installed_version: result.installedVersion,
|
||||
exact_release: true,
|
||||
maintenance_read_only: true,
|
||||
};
|
||||
} finally {
|
||||
server.closeAllConnections();
|
||||
await new Promise((resolveClose, reject) =>
|
||||
server.close((error) =>
|
||||
error === undefined ? resolveClose() : reject(error),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { verifyPackageDiscovery } from "./verify-package-discovery.mjs";
|
||||
import { verifyPackageEnvironment } from "./verify-package-environment.mjs";
|
||||
import { verifyPackageEvidence } from "./verify-package-evidence.mjs";
|
||||
import { verifyPackageInstall } from "./verify-package-install.mjs";
|
||||
import { verifyPackageUpdate } from "./verify-package-update.mjs";
|
||||
import { verifyPackageMcp } from "./verify-package-mcp.mjs";
|
||||
import { verifyPackagePack } from "./verify-package-pack.mjs";
|
||||
import { verifyPackagePlatform } from "./verify-package-platform.mjs";
|
||||
@@ -32,6 +33,7 @@ const referenceRoot = join(workspace, "reference-source");
|
||||
let tarball;
|
||||
let mcpStartup;
|
||||
let mcpModuleLoading;
|
||||
let update;
|
||||
|
||||
try {
|
||||
({ tarball } = await verifyPackagePack({ root, workspace }));
|
||||
@@ -48,6 +50,18 @@ try {
|
||||
workspace,
|
||||
environment: environmentData.environment,
|
||||
});
|
||||
update = await verifyPackageUpdate({
|
||||
prefix: environmentData.prefix,
|
||||
packageRoot: join(
|
||||
environmentData.prefix,
|
||||
"lib",
|
||||
"node_modules",
|
||||
PRODUCT_IDENTITY.packageName,
|
||||
),
|
||||
tarball: join(root, tarball),
|
||||
workspace,
|
||||
environment: environmentData.environment,
|
||||
});
|
||||
mcpStartup = await measureMcpStartup({
|
||||
command: cli,
|
||||
args: ["mcp"],
|
||||
@@ -65,11 +79,12 @@ try {
|
||||
policy: MCP_STARTUP_POLICY,
|
||||
packageName: PRODUCT_IDENTITY.packageName,
|
||||
});
|
||||
const { supportedSetupHost } = await verifyPackageDiscovery({
|
||||
cli,
|
||||
environment: environmentData.environment,
|
||||
});
|
||||
await verifyPackageArtifactAndElectron({
|
||||
const { supportedSetupHost, hopperSetupSupported } =
|
||||
await verifyPackageDiscovery({
|
||||
cli,
|
||||
environment: environmentData.environment,
|
||||
});
|
||||
const { artifactArchive } = await verifyPackageArtifactAndElectron({
|
||||
cli,
|
||||
workspace,
|
||||
environment: environmentData.environment,
|
||||
@@ -109,15 +124,17 @@ try {
|
||||
codexTarget: environmentData.codexTarget,
|
||||
cursorTarget: environmentData.cursorTarget,
|
||||
supportedSetupHost,
|
||||
hopperSetupSupported,
|
||||
root,
|
||||
});
|
||||
await verifyPackageMcp({
|
||||
cli,
|
||||
environment: environmentData.environment,
|
||||
evidenceRoot,
|
||||
artifactArchive,
|
||||
});
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), cli: true, analysisCli: true, artifactCli: true, managedCli: true, managedReconstructionCli: true, managedNativeVerificationCli: true, managedRuntimePlanCli: true, managedApplicationGraphCli: true, evidenceCli: true, incurMcpCommand: PRODUCT_IDENTITY.mcpCommand, lifecycleScriptsRequired: false, doctor: "platform-appropriate", setup: supportedSetupHost ? "planned-then-idempotent" : "unsupported-host-rejected", setupPlanReadOnly: supportedSetupHost, existingHopperPreserved: supportedSetupHost, clients: supportedSetupHost ? 3 : 0, backupReadback: supportedSetupHost, failureRecovery: supportedSetupHost, configSymlinkLifecycle: supportedSetupHost, skill: supportedSetupHost, skillReferences: supportedSetupHost, mcpTools: TOOL_CONTRACTS.length, mcpPrompts: prompts.names.length, promptCompletion: true, promptCompletionLifecycle: true, evidenceMcp: true, targetFree: true, targetLifecycle: true, boundedRegexBridge: true, mcpStartup, mcpModuleLoading })}\n`,
|
||||
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), cli: true, analysisCli: true, artifactCli: true, managedCli: true, managedReconstructionCli: true, managedNativeVerificationCli: true, managedApplicationGraphCli: true, evidenceCli: true, incurMcpCommand: PRODUCT_IDENTITY.mcpCommand, lifecycleScriptsRequired: false, doctor: "platform-appropriate", setup: supportedSetupHost ? "planned-then-idempotent" : "unsupported-host-rejected", setupPlanReadOnly: supportedSetupHost, existingHopperPreserved: supportedSetupHost, clients: supportedSetupHost ? 4 : 0, backupReadback: supportedSetupHost, failureRecovery: supportedSetupHost, configSymlinkLifecycle: supportedSetupHost, skill: supportedSetupHost, skillReferences: supportedSetupHost, mcpTools: TOOL_CONTRACTS.length, mcpPrompts: prompts.names.length, promptCompletion: true, promptCompletionLifecycle: true, evidenceMcp: true, targetFree: true, targetLifecycle: true, boundedRegexBridge: true, update, mcpStartup, mcpModuleLoading })}\n`,
|
||||
);
|
||||
} finally {
|
||||
if (tarball) await rm(join(root, tarball), { force: true });
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { delimiter, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
import { lt, rcompare } from "semver";
|
||||
|
||||
@@ -44,7 +45,7 @@ transport.stderr?.on("data", (chunk) => {
|
||||
});
|
||||
|
||||
let publishedToolCount = 0;
|
||||
let upgrade;
|
||||
let update;
|
||||
|
||||
try {
|
||||
await client.connect(transport);
|
||||
@@ -71,7 +72,7 @@ try {
|
||||
});
|
||||
if (status.isError === true)
|
||||
throw new Error("Published target-free MCP session check failed");
|
||||
upgrade = await verifyPublishedUpgrade({ version, canaryRoot });
|
||||
update = await verifyPublishedUpdate({ version, canaryRoot });
|
||||
} catch (cause) {
|
||||
throw new Error(`Published package verification failed: ${stderr}`, {
|
||||
cause,
|
||||
@@ -86,17 +87,17 @@ try {
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), package: "rea-agents", version, mcpTools: publishedToolCount, canonicalMcpTools: CATALOG_IDENTITY.counts.mcp_tools, upgrade })}\n`,
|
||||
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), package: "rea-agents", version, mcpTools: publishedToolCount, canonicalMcpTools: CATALOG_IDENTITY.counts.mcp_tools, update })}\n`,
|
||||
);
|
||||
|
||||
async function verifyPublishedUpgrade({ version: targetVersion, canaryRoot }) {
|
||||
async function verifyPublishedUpdate({ version: targetVersion, canaryRoot }) {
|
||||
const previousVersion = await findPreviousPublishedVersion(targetVersion);
|
||||
const prefix = join(canaryRoot, "upgrade-prefix");
|
||||
const home = join(canaryRoot, "upgrade-home");
|
||||
const prefix = join(canaryRoot, "update-prefix");
|
||||
const home = join(canaryRoot, "update-home");
|
||||
const environment = {
|
||||
...process.env,
|
||||
HOME: home,
|
||||
npm_config_cache: join(canaryRoot, "upgrade-cache"),
|
||||
npm_config_cache: join(canaryRoot, "update-cache"),
|
||||
npm_config_prefix: prefix,
|
||||
PATH: `${join(prefix, "bin")}${delimiter}${process.env.PATH ?? ""}`,
|
||||
};
|
||||
@@ -114,33 +115,63 @@ async function verifyPublishedUpgrade({ version: targetVersion, canaryRoot }) {
|
||||
{ cwd: canaryRoot, env: environment, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
|
||||
const { stdout } = await execFileAsync(
|
||||
"npx",
|
||||
["rea-agents", "upgrade", "--json"],
|
||||
{ cwd: canaryRoot, env: environment, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const result = JSON.parse(stdout);
|
||||
if (
|
||||
result.status !== "upgraded" ||
|
||||
result.previousVersion !== previousVersion ||
|
||||
result.latestVersion !== targetVersion
|
||||
)
|
||||
throw new Error(
|
||||
`npx rea-agents upgrade did not upgrade ${previousVersion} to ${targetVersion}: ${stdout}`,
|
||||
);
|
||||
|
||||
const npmRoot = (
|
||||
await execFileAsync("npm", ["root", "--global"], {
|
||||
cwd: canaryRoot,
|
||||
env: environment,
|
||||
})
|
||||
).stdout.trim();
|
||||
// Older releases may predate the update command. Exercise the new published
|
||||
// updater's production workflow against that real previous installation.
|
||||
const updaterPrefix = join(canaryRoot, "updater");
|
||||
await execFileAsync(
|
||||
"npm",
|
||||
[
|
||||
"install",
|
||||
"--ignore-scripts",
|
||||
"--no-save",
|
||||
"--no-package-lock",
|
||||
"--prefix",
|
||||
updaterPrefix,
|
||||
`rea-agents@${targetVersion}`,
|
||||
],
|
||||
{ cwd: canaryRoot, env: environment, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const updaterRoot = join(updaterPrefix, "node_modules", "rea-agents");
|
||||
const harness = join(canaryRoot, "update.mjs");
|
||||
await writeFile(
|
||||
harness,
|
||||
`
|
||||
import { runUpdate } from ${JSON.stringify(pathToFileURL(join(updaterRoot, "dist/application/Update.js")).href)};
|
||||
import { systemUpdateHost } from ${JSON.stringify(pathToFileURL(join(updaterRoot, "dist/application/UpdateRuntime.js")).href)};
|
||||
const result = await runUpdate(${JSON.stringify(previousVersion)}, systemUpdateHost(${JSON.stringify(join(npmRoot, "rea-agents"))}, ${JSON.stringify(home)}), "structured");
|
||||
process.stdout.write(JSON.stringify(result));
|
||||
if (result.status === "failed") process.exitCode = 1;
|
||||
`,
|
||||
);
|
||||
const { stdout } = await execFileAsync(process.execPath, [harness], {
|
||||
cwd: canaryRoot,
|
||||
env: environment,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
});
|
||||
const result = JSON.parse(stdout);
|
||||
if (
|
||||
result.status !== "updated" ||
|
||||
result.previousVersion !== previousVersion ||
|
||||
result.latestVersion !== targetVersion ||
|
||||
result.installedVersion !== targetVersion ||
|
||||
result.command.at(-1) !== `rea-agents@${targetVersion}`
|
||||
)
|
||||
throw new Error(
|
||||
`Published REA updater did not update ${previousVersion} to ${targetVersion}: ${stdout}`,
|
||||
);
|
||||
|
||||
const installed = JSON.parse(
|
||||
await readFile(join(npmRoot, "rea-agents", "package.json"), "utf8"),
|
||||
);
|
||||
if (installed.version !== targetVersion)
|
||||
throw new Error(
|
||||
`npx rea-agents upgrade installed ${installed.version}, expected ${targetVersion}`,
|
||||
`REA update installed ${installed.version}, expected ${targetVersion}`,
|
||||
);
|
||||
|
||||
const help = (
|
||||
@@ -149,8 +180,10 @@ async function verifyPublishedUpgrade({ version: targetVersion, canaryRoot }) {
|
||||
env: environment,
|
||||
})
|
||||
).stdout;
|
||||
if (!help.includes("setup"))
|
||||
throw new Error("the upgraded rea-agents executable did not start");
|
||||
if (!/^\s{2}update\s/mu.test(help) || /^\s{2}upgrade\s/mu.test(help))
|
||||
throw new Error(
|
||||
"the updated executable did not expose only the update command",
|
||||
);
|
||||
|
||||
return {
|
||||
status: result.status,
|
||||
|
||||
@@ -34,15 +34,13 @@ import {
|
||||
} from "./lib/browser-scenario-verifier.mjs";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { verifyLargeScreenshotE2e } from "./lib/browser-screenshot-e2e.mjs";
|
||||
import { verifyPopupEventCoverage } from "./lib/browser-popup-e2e.mjs";
|
||||
|
||||
const SECRET_VALUES = [
|
||||
"network-secret-value",
|
||||
"console-secret-value",
|
||||
"storage-secret-value",
|
||||
"websocket-secret-value",
|
||||
"ax-private-label-value",
|
||||
];
|
||||
const REAL_BROWSER_STARTUP_TIMEOUT_MS = 30_000;
|
||||
const SCENARIO_SECRET_VALUE = "rea-browser-verifier-secret";
|
||||
const SCENARIO_URL_SECRET_VALUE = "rea-browser-url-verifier-secret";
|
||||
process.env.REA_BROWSER_VERIFIER_SECRET = SCENARIO_SECRET_VALUE;
|
||||
process.env.REA_BROWSER_VERIFIER_URL = SCENARIO_URL_SECRET_VALUE;
|
||||
const verifierRun = createVerifierRun();
|
||||
|
||||
const executable = await browserExecutable();
|
||||
@@ -85,7 +83,7 @@ try {
|
||||
});
|
||||
const endpoint = `http://127.0.0.1:${String(port)}`;
|
||||
const provider = new CdpBrowserProvider();
|
||||
const target = await pageTarget(provider, endpoint, site.origin);
|
||||
const target = await pageTarget(provider, endpoint);
|
||||
const observed = await provider.inspectPage(
|
||||
inspectWebPageInputSchema.parse({
|
||||
cdp_endpoint: endpoint,
|
||||
@@ -99,10 +97,25 @@ try {
|
||||
if (!observed.ok) throw observed.error;
|
||||
assertObservation(observed.value, site.origin);
|
||||
const serialized = JSON.stringify(observed.value);
|
||||
for (const secret of [...SECRET_VALUES, "browser-secret-value"])
|
||||
if (
|
||||
!observed.value.target.url.includes("startup=browser-secret-value") ||
|
||||
!observed.value.network.requests.some((request) =>
|
||||
request.url.includes("token=network-secret-value"),
|
||||
)
|
||||
)
|
||||
throw new Error(
|
||||
"Passive browser result did not preserve ordinary URL queries",
|
||||
);
|
||||
for (const secret of [
|
||||
"request-secret-value",
|
||||
"request-body-secret-value",
|
||||
"response-secret-value",
|
||||
"storage-secret-value",
|
||||
"websocket-secret-value",
|
||||
])
|
||||
if (serialized.includes(secret))
|
||||
throw new Error(
|
||||
`Passive browser result retained redacted value: ${secret}`,
|
||||
`Passive browser result included unselected structured data: ${secret}`,
|
||||
);
|
||||
|
||||
const withSource = await provider.inspectPage(
|
||||
@@ -217,17 +230,12 @@ try {
|
||||
mode: "connect",
|
||||
cdp_endpoint: endpoint,
|
||||
target_id: target,
|
||||
ownership: "external",
|
||||
cleanup: "disconnect-only",
|
||||
},
|
||||
site.origin,
|
||||
);
|
||||
let attachedScenario;
|
||||
try {
|
||||
attachedScenario = await runScenarioCli(attachedScenarioInput, {
|
||||
REA_BROWSER_SCENARIO_CDP_ENDPOINTS_JSON: JSON.stringify([endpoint]),
|
||||
REA_BROWSER_SCENARIO_ALLOWED_ORIGINS_JSON: JSON.stringify([site.origin]),
|
||||
});
|
||||
attachedScenario = await runScenarioCli(attachedScenarioInput);
|
||||
} catch (cliError) {
|
||||
const direct =
|
||||
await new PlaywrightBrowserScenarioProvider().captureScenario(
|
||||
@@ -239,12 +247,8 @@ try {
|
||||
underlying instanceof Error
|
||||
? `${underlying.name}: ${underlying.message}`
|
||||
: String(underlying ?? direct.error.message);
|
||||
const redactedDetails = [...SECRET_VALUES, "browser-secret-value"].reduce(
|
||||
(message, secret) => message.replaceAll(secret, "<redacted>"),
|
||||
details,
|
||||
);
|
||||
throw new Error(
|
||||
`Attached browser scenario failed through both CLI and provider: ${redactedDetails}`,
|
||||
`Attached browser scenario failed through both CLI and provider: ${details}`,
|
||||
{ cause: cliError },
|
||||
);
|
||||
}
|
||||
@@ -273,9 +277,6 @@ try {
|
||||
{
|
||||
mode: "launch",
|
||||
executable_path: executable,
|
||||
headless: true,
|
||||
user_data: "temporary-owned",
|
||||
cleanup: "close-and-delete-profile",
|
||||
},
|
||||
site.origin,
|
||||
),
|
||||
@@ -290,15 +291,35 @@ try {
|
||||
if ([...profilesAfter].some((entry) => !profilesBefore.has(entry)))
|
||||
throw new Error("Scenario launch retained a temporary browser profile");
|
||||
assertScenarioCapture(launchedScenario.value);
|
||||
for (const secret of ["network-secret-value", "websocket-url-secret"])
|
||||
if (
|
||||
JSON.stringify([attachedScenario, launchedScenario.value]).includes(
|
||||
secret,
|
||||
)
|
||||
)
|
||||
throw new Error(`Browser scenario retained sensitive value: ${secret}`);
|
||||
const scenarioResults = JSON.stringify([
|
||||
attachedScenario,
|
||||
launchedScenario.value,
|
||||
]);
|
||||
for (const retainedValue of [
|
||||
"token=network-secret-value",
|
||||
"token=websocket-url-secret",
|
||||
"authorization=Bearer console-secret-value",
|
||||
"websocket-secret-value",
|
||||
])
|
||||
if (!scenarioResults.includes(retainedValue))
|
||||
throw new Error(
|
||||
`Browser scenario did not preserve selected value: ${retainedValue}`,
|
||||
);
|
||||
for (const unselectedValue of [
|
||||
SCENARIO_SECRET_VALUE,
|
||||
SCENARIO_URL_SECRET_VALUE,
|
||||
"request-secret-value",
|
||||
"request-body-secret-value",
|
||||
"response-secret-value",
|
||||
"storage-secret-value",
|
||||
])
|
||||
if (scenarioResults.includes(unselectedValue))
|
||||
throw new Error(
|
||||
`Browser scenario included unselected structured value: ${unselectedValue}`,
|
||||
);
|
||||
|
||||
const largeScreenshot = await verifyLargeScreenshotE2e(endpoint, site.origin);
|
||||
const popupEvents = await verifyPopupEventCoverage(executable);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
verifier_run: await completeVerifierRun(verifierRun),
|
||||
@@ -317,6 +338,7 @@ try {
|
||||
pageScopedTransport: true,
|
||||
screenshotBytes: screenshot.value.artifact.bytes,
|
||||
largeScreenshot,
|
||||
popupEvents,
|
||||
browserScenarioCli: true,
|
||||
browserScenarioAttachCleanup: "disconnected-external",
|
||||
browserScenarioLaunchCleanup: "terminated-owned-process",
|
||||
@@ -336,7 +358,7 @@ try {
|
||||
}
|
||||
|
||||
async function verifyPageScopedTransport(provider, proxy, origin) {
|
||||
const target = await pageTarget(provider, proxy.endpoint, origin);
|
||||
const target = await pageTarget(provider, proxy.endpoint);
|
||||
const input = inspectWebPageInputSchema.parse({
|
||||
cdp_endpoint: proxy.endpoint,
|
||||
allowed_origins: [origin],
|
||||
@@ -413,12 +435,11 @@ async function browserExecutable() {
|
||||
);
|
||||
}
|
||||
|
||||
async function pageTarget(provider, endpoint, origin) {
|
||||
async function pageTarget(provider, endpoint) {
|
||||
for (let attempt = 0; attempt < 100; attempt += 1) {
|
||||
const listed = await provider.listTargets(
|
||||
listBrowserTargetsInputSchema.parse({
|
||||
cdp_endpoint: endpoint,
|
||||
allowed_origins: [origin],
|
||||
}),
|
||||
);
|
||||
if (listed.ok && listed.value.targets[0] !== undefined)
|
||||
|
||||
@@ -10,7 +10,7 @@ import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
const executable = process.env.REA_ELECTRON_EXECUTABLE;
|
||||
if (executable === undefined || executable.length === 0)
|
||||
throw new Error(
|
||||
"REA_ELECTRON_EXECUTABLE must be an absolute path to an approved Electron executable",
|
||||
"REA_ELECTRON_EXECUTABLE must identify the Electron executable for this verification lane",
|
||||
);
|
||||
|
||||
const repositoryRoot = fileURLToPath(new URL("..", import.meta.url));
|
||||
@@ -46,7 +46,6 @@ const input = electronActiveObservationInputSchema.parse({
|
||||
window_index: 1,
|
||||
},
|
||||
],
|
||||
approved: true,
|
||||
});
|
||||
const verifierRun = createVerifierRun();
|
||||
const result = await new PlaywrightElectronActiveProvider().capture(input);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { hasTypedSwitchEvidence } from "./verify-real-ghidra-switch-assertions.mjs";
|
||||
|
||||
import { access } from "node:fs/promises";
|
||||
|
||||
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
@@ -107,6 +109,14 @@ export function assertDenseSwitchDossier(dossier, address) {
|
||||
);
|
||||
if (
|
||||
denseTable === undefined ||
|
||||
denseTable.default_targets.length !== 1 ||
|
||||
denseTable.default_targets[0].confidence !== "high" ||
|
||||
!hasTypedSwitchEvidence(
|
||||
denseTable.default_targets[0].evidence,
|
||||
"Typed default label",
|
||||
denseTable.default_targets[0].target_address,
|
||||
denseTable.dispatch_address,
|
||||
) ||
|
||||
denseTable.mappings.some(
|
||||
({ target_address: target, evidence }) =>
|
||||
!/^0x[0-9a-f]+$/u.test(target) ||
|
||||
@@ -146,9 +156,8 @@ function assertNativeApiBoundary(observed, requireJumpTable) {
|
||||
) &&
|
||||
mappings.length > 0 &&
|
||||
mappings.every(
|
||||
({ target_address: target, data_addresses: addresses, evidence }) =>
|
||||
({ target_address: target, evidence }) =>
|
||||
/^0x[0-9a-f]+$/u.test(target) &&
|
||||
addresses.every((address) => /^0x[0-9a-f]+$/u.test(address)) &&
|
||||
evidence.some(({ kind }) => kind === "jump-table"),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,502 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
mkdtemp,
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { isAbsolute, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { parseEvidence } from "../dist/domain/evidence.js";
|
||||
import {
|
||||
functionBodySchema,
|
||||
functionDossierSchema,
|
||||
} from "../dist/domain/hopperValues.js";
|
||||
import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js";
|
||||
import { buildDosMzFixture } from "../tests/conformance/ghidra/dos-mz-fixture.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
|
||||
// This lane intentionally has no game paths, binary seeds or compiler dependency.
|
||||
if (process.argv.length !== 2)
|
||||
throw new Error("Usage: node scripts/verify-real-ghidra-dos.mjs");
|
||||
if (!["linux", "darwin"].includes(process.platform))
|
||||
throw new Error(
|
||||
"verify:ghidra:dos supports Linux/macOS hosts only; Windows P0 is PE x86-64 only.",
|
||||
);
|
||||
if (process.env.GHIDRA_INSTALL_DIR === undefined)
|
||||
throw new Error(
|
||||
"verify:ghidra:dos prerequisite missing: GHIDRA_INSTALL_DIR (bring your own Ghidra).",
|
||||
);
|
||||
const installation = inspectGhidraInstallation({
|
||||
installDir: process.env.GHIDRA_INSTALL_DIR,
|
||||
...(process.env.JAVA_HOME === undefined
|
||||
? {}
|
||||
: { javaHome: process.env.JAVA_HOME }),
|
||||
});
|
||||
if (installation.status !== "available")
|
||||
throw new Error(
|
||||
`verify:ghidra:dos prerequisite unavailable: ${JSON.stringify(installation)}`,
|
||||
);
|
||||
const captureDirectory = process.env.REA_DOS_PROOF_CAPTURE_DIR;
|
||||
if (captureDirectory !== undefined) {
|
||||
assert.ok(
|
||||
isAbsolute(captureDirectory),
|
||||
"REA_DOS_PROOF_CAPTURE_DIR must be absolute",
|
||||
);
|
||||
await mkdir(captureDirectory, { recursive: true, mode: 0o700 });
|
||||
}
|
||||
const run = createVerifierRun();
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-dos-proof-"));
|
||||
const runtime = join(workspace, "runtime");
|
||||
await mkdir(runtime);
|
||||
const fixture = buildDosMzFixture();
|
||||
const targetPath = join(workspace, "fixture.exe");
|
||||
await writeFile(targetPath, fixture.bytes, { mode: 0o600 });
|
||||
const sha256 = digest(fixture.bytes);
|
||||
const entrypoint = fileURLToPath(new URL("./rea.mjs", import.meta.url));
|
||||
const env = {
|
||||
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
||||
TMPDIR: runtime,
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_ANALYSIS_PROVIDER: "ghidra",
|
||||
REA_PROCESS_RUN_ID: run.run_id,
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
|
||||
GHIDRA_INSTALL_DIR: process.env.GHIDRA_INSTALL_DIR,
|
||||
...(process.env.JAVA_HOME === undefined
|
||||
? {}
|
||||
: { JAVA_HOME: process.env.JAVA_HOME }),
|
||||
};
|
||||
const address = (offset) =>
|
||||
`0x${(fixture.load_segment * 16 + offset).toString(16)}`;
|
||||
const expected = {
|
||||
entry: address(fixture.entry_offset),
|
||||
near: address(fixture.near_offset),
|
||||
far: address(fixture.far_offset),
|
||||
};
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "rea-real-dos-proof", version: "1" });
|
||||
let opened = false;
|
||||
let stderr = "";
|
||||
let report;
|
||||
try {
|
||||
await client.connect(transport);
|
||||
transport.stderr?.on("data", (chunk) => {
|
||||
stderr = (stderr + chunk.toString()).slice(-65536);
|
||||
});
|
||||
const target = await call("open_binary", {
|
||||
path: targetPath,
|
||||
provider: "ghidra",
|
||||
});
|
||||
opened = true;
|
||||
assert.equal(target.format, "dos-mz");
|
||||
assert.equal(target.kind, "executable");
|
||||
assert.equal(target.architecture, "x86");
|
||||
assert.equal(target.sha256, sha256);
|
||||
const procedures = await call("list_procedures");
|
||||
assert.ok(
|
||||
Array.isArray(procedures),
|
||||
"Public procedure inventory is not an array",
|
||||
);
|
||||
const identities = Object.fromEntries(
|
||||
Object.entries(expected).map(([key, value]) => {
|
||||
const item = procedures.find((candidate) => candidate.address === value);
|
||||
assert.ok(
|
||||
item,
|
||||
`Source-owned ${key} procedure ${value} was not discovered from MZ entry/call flow`,
|
||||
);
|
||||
return [key, item];
|
||||
}),
|
||||
);
|
||||
const session = await call("binary_session");
|
||||
const profile = session.analysis_provider_binding?.analysis_profile;
|
||||
assert.ok(profile, "Public session omitted committed profile");
|
||||
assertProfile(profile);
|
||||
assert.equal(session.sha256, sha256);
|
||||
assert.equal(session.open, true);
|
||||
const facts = {};
|
||||
for (const [key, identity] of Object.entries(identities)) {
|
||||
const procedure = identity.address; // Use returned addresses, never symbol-name heuristics.
|
||||
assert.match(procedure, /^0x[0-9a-f]+$/u);
|
||||
const resolved = await call("procedure_address", { procedure });
|
||||
assert.equal(resolved, procedure, "Flat address round-trip drifted");
|
||||
const info = await call("procedure_info", { procedure });
|
||||
const dossierResult = await evidenceCall("analyze_function", { procedure });
|
||||
const dossier = functionDossierSchema.parse(
|
||||
dossierResult.normalized_result,
|
||||
);
|
||||
assertBody(info.body, procedure);
|
||||
assertBody(dossier.procedure.body, procedure);
|
||||
assert.equal(info.entrypoint, procedure);
|
||||
assert.equal(info.length, info.body.total_bytes);
|
||||
assert.deepEqual(dossier.procedure.body, info.body);
|
||||
const containingQueries = [procedure, info.body.ranges.at(-1).end];
|
||||
for (const query of containingQueries) {
|
||||
const containing = await call("resolve_containing_procedure", {
|
||||
address: query,
|
||||
});
|
||||
assert.equal(containing.found, true);
|
||||
assert.equal(containing.query_address, query);
|
||||
assert.equal(containing.procedure.address, procedure);
|
||||
assert.deepEqual(containing.procedure.body, info.body);
|
||||
}
|
||||
if (key === "entry") {
|
||||
const gap = await call("resolve_containing_procedure", {
|
||||
address: address(0x10),
|
||||
});
|
||||
assert.equal(
|
||||
gap.found,
|
||||
false,
|
||||
"Gap inside enclosing span was treated as function body",
|
||||
);
|
||||
assert.equal(gap.reason, "not_in_procedure");
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
dossier.pseudocode.trim().length > 0,
|
||||
"Real DOS decompilation returned empty pseudocode",
|
||||
);
|
||||
assert.equal(dossier.procedure.address, procedure);
|
||||
const window = await call("read_function_instructions", { procedure });
|
||||
assert.deepEqual(window.procedure.body, info.body);
|
||||
assert.ok(window.instructions.length > 0);
|
||||
if (key === "entry") {
|
||||
assert.deepEqual(info.body.ranges, [
|
||||
{ start: procedure, end: address(fixture.entry_first_bytes - 1) },
|
||||
{
|
||||
start: address(fixture.entry_tail_offset),
|
||||
end: address(
|
||||
fixture.entry_tail_offset + fixture.entry_tail_bytes - 1,
|
||||
),
|
||||
},
|
||||
]);
|
||||
assert.equal(
|
||||
info.body.total_bytes,
|
||||
fixture.entry_first_bytes + fixture.entry_tail_bytes,
|
||||
);
|
||||
assert.equal(
|
||||
info.body.span_bytes,
|
||||
fixture.entry_tail_offset + fixture.entry_tail_bytes,
|
||||
);
|
||||
assert.equal(info.body.non_contiguous, true);
|
||||
} else {
|
||||
const size =
|
||||
key === "near" ? fixture.near_body_bytes : fixture.far_body_bytes;
|
||||
assert.deepEqual(info.body.ranges, [
|
||||
{
|
||||
start: procedure,
|
||||
end: address(
|
||||
(key === "near" ? fixture.near_offset : fixture.far_offset) +
|
||||
size -
|
||||
1,
|
||||
),
|
||||
},
|
||||
]);
|
||||
assert.equal(info.body.total_bytes, size);
|
||||
assert.equal(info.body.span_bytes, size);
|
||||
}
|
||||
const cli = await cliEvidence("function", procedure);
|
||||
if (captureDirectory !== undefined) {
|
||||
await writeFile(
|
||||
join(captureDirectory, `public-fixture-${key}-cli.json`),
|
||||
`${JSON.stringify(cli, null, 2)}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
await writeFile(
|
||||
join(captureDirectory, `public-fixture-${key}-mcp.json`),
|
||||
`${JSON.stringify(dossierResult, null, 2)}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
}
|
||||
const cliDossier = functionDossierSchema.parse(cli.normalized_result);
|
||||
assert.deepEqual(
|
||||
stableDossierObservations(cliDossier),
|
||||
stableDossierObservations(dossier),
|
||||
"CLI/MCP stable DOS dossier observations differ",
|
||||
);
|
||||
facts[key] = {
|
||||
address: procedure,
|
||||
body: info.body,
|
||||
instruction_count: window.instructions.length,
|
||||
pseudocode_nonempty: true,
|
||||
pseudocode: dossier.pseudocode,
|
||||
cli_mcp_observations_equal: true,
|
||||
containing_procedure_body_equal: true,
|
||||
limitations: dossier.limitations,
|
||||
};
|
||||
}
|
||||
const move = await call("inspect_native_instruction", {
|
||||
address: identities.entry.address,
|
||||
});
|
||||
assert.equal(move.status, "decoded");
|
||||
assert.equal(move.length, 3);
|
||||
assert.equal(hexBytes(move.bytes), "b83412");
|
||||
assert.match(move.mnemonic, /^MOV$/iu);
|
||||
assert.ok(
|
||||
move.operands
|
||||
.flatMap((operand) => operand.components)
|
||||
.some(
|
||||
(part) =>
|
||||
part.kind === "register" &&
|
||||
/^AX$/iu.test(part.text) &&
|
||||
part.bit_width === 16,
|
||||
),
|
||||
"DOS entry was not decoded as 16-bit AX",
|
||||
);
|
||||
const nearMemory = await call("inspect_native_instruction", {
|
||||
address: identities.near.address,
|
||||
});
|
||||
assert.equal(nearMemory.status, "decoded");
|
||||
assert.equal(nearMemory.length, 2);
|
||||
assert.equal(hexBytes(nearMemory.bytes), "fe00");
|
||||
assert.match(nearMemory.mnemonic, /^INC$/iu);
|
||||
const nearCall = await call("inspect_native_instruction", {
|
||||
address: address(fixture.near_call_offset),
|
||||
});
|
||||
const farCall = await call("inspect_native_instruction", {
|
||||
address: address(fixture.far_call_offset),
|
||||
});
|
||||
assert.equal(hexBytes(nearCall.bytes), "e81a00");
|
||||
// Source far segment 0004 is relocated by the independently fixed 1000 load segment.
|
||||
assert.equal(hexBytes(farCall.bytes), "9a00000410");
|
||||
for (const [key, instruction] of [
|
||||
["near", nearCall],
|
||||
["far", farCall],
|
||||
]) {
|
||||
assert.equal(instruction.flow.kind, "call");
|
||||
assert.ok(
|
||||
instruction.flow.direct_destinations.includes(identities[key].address),
|
||||
);
|
||||
const targets = await call("resolve_native_call_targets", {
|
||||
address: instruction.address,
|
||||
});
|
||||
assert.equal(targets.status, "direct");
|
||||
assert.ok(
|
||||
targets.targets.some(
|
||||
(target) => target.address === identities[key].address,
|
||||
),
|
||||
);
|
||||
}
|
||||
const invalid = await client.callTool({
|
||||
name: "procedure_info",
|
||||
arguments: { procedure: "0xffffffffffffffff" },
|
||||
});
|
||||
assert.equal(invalid.isError, true, "Unmapped address did not fail");
|
||||
const cliWindow = await cliEvidence("instructions", identities.entry.address);
|
||||
assert.deepEqual(
|
||||
cliWindow.normalized_result,
|
||||
await call("read_function_instructions", {
|
||||
procedure: identities.entry.address,
|
||||
}),
|
||||
);
|
||||
assert.equal(
|
||||
digest(await readFile(targetPath)),
|
||||
sha256,
|
||||
"Analysis changed original bytes",
|
||||
);
|
||||
await call("close_binary");
|
||||
opened = false;
|
||||
assert.equal((await call("binary_session")).open, false);
|
||||
report = {
|
||||
ok: true,
|
||||
mocked: false,
|
||||
lane: "ghidra-dos",
|
||||
host: process.platform,
|
||||
provider: { id: "ghidra", version: installation.providerVersion },
|
||||
target: {
|
||||
format: "dos-mz",
|
||||
architecture: "x86",
|
||||
sha256,
|
||||
bytes: fixture.bytes.length,
|
||||
},
|
||||
profile: {
|
||||
digest: profile.digest,
|
||||
loader: profile.parameters.loader,
|
||||
language_id: profile.parameters.language_id,
|
||||
compiler_spec_id: profile.parameters.compiler_spec_id,
|
||||
load_segment: profile.parameters.load_segment,
|
||||
},
|
||||
transport: { cli: true, stdio_mcp: true },
|
||||
functions: facts,
|
||||
instruction_oracles: {
|
||||
entry_bytes: hexBytes(move.bytes),
|
||||
near_memory_bytes: hexBytes(nearMemory.bytes),
|
||||
near_call_bytes: hexBytes(nearCall.bytes),
|
||||
relocated_far_call_bytes: hexBytes(farCall.bytes),
|
||||
near_target: expected.near,
|
||||
far_target: expected.far,
|
||||
},
|
||||
invalid_address_rejected: true,
|
||||
original_unchanged: true,
|
||||
limitations: [
|
||||
"Read-only import/decompilation evidence, not DOS runtime execution or whole-program semantic equivalence.",
|
||||
"The complete body is Ghidra's observed AddressSet, not independently proven historical compiler ownership.",
|
||||
"Language/compiler identity is bound by the production authenticated bridge handshake; profile loader is the declared import policy.",
|
||||
"Native high-pcode flow is excluded from cross-runtime parity: LOAD/STORE address-space selector constants are process-specific in current Ghidra output. Both dossiers are still strictly parsed.",
|
||||
"Windows P0 remains unsupported for DOS MZ.",
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
if (stderr.trim())
|
||||
process.stderr.write(`DOS verifier MCP diagnostics:\n${stderr}\n`);
|
||||
throw error;
|
||||
} finally {
|
||||
try {
|
||||
if (opened) {
|
||||
try {
|
||||
await call("close_binary");
|
||||
} catch (error) {
|
||||
process.stderr.write(`DOS cleanup close failed: ${String(error)}\n`);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await client.close();
|
||||
} finally {
|
||||
await transport.close();
|
||||
}
|
||||
assert.equal(
|
||||
digest(await readFile(targetPath)),
|
||||
sha256,
|
||||
"Original changed during cleanup",
|
||||
);
|
||||
const leftovers = (await readdir(runtime)).filter((name) =>
|
||||
name.startsWith("rea-ghidra-"),
|
||||
);
|
||||
assert.deepEqual(leftovers, [], "Owned Ghidra runtime survived close");
|
||||
} finally {
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
assert.ok(report, "No DOS proof report was produced");
|
||||
const verifierRun = await completeVerifierRun(run);
|
||||
if (verifierRun.process_lineage.status === "verified")
|
||||
assert.deepEqual(
|
||||
verifierRun.process_lineage.descendants,
|
||||
[],
|
||||
"Owned descendants survived verifier cleanup",
|
||||
);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ verifier_run: verifierRun, ...report, cleanup: "complete" })}\n`,
|
||||
);
|
||||
|
||||
function digest(bytes) {
|
||||
return createHash("sha256").update(bytes).digest("hex");
|
||||
}
|
||||
function hexBytes(bytes) {
|
||||
assert.equal(typeof bytes, "string");
|
||||
return bytes.replace(/\s/gu, "").toLowerCase();
|
||||
}
|
||||
function assertProfile(profile) {
|
||||
assert.equal(
|
||||
profile.parameters.function_body_evidence,
|
||||
"complete-inclusive-ranges-v1",
|
||||
);
|
||||
assert.equal(profile.provider.id, "ghidra");
|
||||
assert.equal(profile.parameters.target_format, "dos-mz");
|
||||
assert.equal(profile.parameters.import_mode, "ephemeral-read-only");
|
||||
assert.equal(profile.parameters.loader, "MzLoader");
|
||||
assert.equal(profile.parameters.language_id, "x86:LE:16:Real Mode");
|
||||
assert.equal(profile.parameters.compiler_spec_id, "default");
|
||||
assert.equal(profile.parameters.load_segment, "0x1000");
|
||||
}
|
||||
function assertBody(input, entry) {
|
||||
const body = functionBodySchema.parse(input);
|
||||
assert.equal(body.available, true);
|
||||
assert.equal(body.provenance, "ghidra-function-body-address-set");
|
||||
assert.equal(body.contains_entry, true);
|
||||
let total = 0n;
|
||||
let previousEnd = null;
|
||||
for (const range of body.ranges) {
|
||||
assert.match(range.start, /^0x[0-9a-f]+$/u);
|
||||
assert.match(range.end, /^0x[0-9a-f]+$/u);
|
||||
const start = BigInt(range.start),
|
||||
end = BigInt(range.end);
|
||||
assert.ok(start <= end);
|
||||
if (previousEnd !== null) assert.ok(start > previousEnd + 1n);
|
||||
previousEnd = end;
|
||||
total += end - start + 1n;
|
||||
}
|
||||
assert.ok(
|
||||
body.ranges.some(
|
||||
(range) =>
|
||||
BigInt(range.start) <= BigInt(entry) &&
|
||||
BigInt(entry) <= BigInt(range.end),
|
||||
),
|
||||
);
|
||||
assert.equal(BigInt(body.total_bytes), total);
|
||||
assert.equal(body.non_contiguous, body.ranges.length > 1);
|
||||
assert.equal(
|
||||
BigInt(body.span_bytes),
|
||||
BigInt(body.ranges.at(-1).end) - BigInt(body.ranges[0].start) + 1n,
|
||||
);
|
||||
}
|
||||
async function call(name, arguments_ = {}) {
|
||||
const result = await client.callTool(
|
||||
{ name, arguments: arguments_ },
|
||||
{ timeout: 240000 },
|
||||
);
|
||||
assert.notEqual(result.isError, true, JSON.stringify(result));
|
||||
assert.ok(
|
||||
result.structuredContent && "result" in result.structuredContent,
|
||||
`${name} omitted structured result`,
|
||||
);
|
||||
return result.structuredContent.result;
|
||||
}
|
||||
async function evidenceCall(name, arguments_) {
|
||||
const result = await client.callTool(
|
||||
{ name, arguments: arguments_ },
|
||||
{ timeout: 240000 },
|
||||
);
|
||||
assert.notEqual(result.isError, true, JSON.stringify(result));
|
||||
const evidence = parseEvidence({
|
||||
...result.structuredContent?.evidence,
|
||||
normalized_result: result.structuredContent?.result,
|
||||
});
|
||||
assert.equal(evidence.subject.digest.sha256, sha256);
|
||||
assert.equal(evidence.provider.id, "rea-workflow");
|
||||
assert.equal(evidence.analysis_profile.provider.id, "rea-workflow");
|
||||
assertProfile(evidence.analysis_profile.parameters.upstream_analysis_profile);
|
||||
return evidence;
|
||||
}
|
||||
async function cliEvidence(command, procedure) {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
command,
|
||||
targetPath,
|
||||
procedure,
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
],
|
||||
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const evidence = parseEvidence(JSON.parse(stdout));
|
||||
assert.equal(evidence.subject.digest.sha256, sha256);
|
||||
assert.equal(evidence.provider.id, "ghidra");
|
||||
assertProfile(evidence.analysis_profile);
|
||||
assert.equal(digest(await readFile(targetPath)), sha256);
|
||||
return evidence;
|
||||
}
|
||||
|
||||
// Preserve all body/instruction/decompilation/reference observations; raw high-pcode
|
||||
// address-space selector tokens are process-specific and are not a semantic oracle.
|
||||
function stableDossierObservations(dossier) {
|
||||
const { native_value_flow: flow, ...observations } = dossier;
|
||||
assert.ok(flow, "Dossier omitted native value-flow availability");
|
||||
return observations;
|
||||
}
|
||||
@@ -613,3 +613,37 @@ export async function verifyNativeValueTrace(client, procedures, target) {
|
||||
const global = requireProcedure(names, "rea_ghidra_inventory_global");
|
||||
return verifyNativeValueE2e(target, entry.address, global.address);
|
||||
}
|
||||
|
||||
/** Match the source default's -1 assignment in supported native compiler encodings. */
|
||||
export async function verifyDenseDefaultReturn(client, targetAddress) {
|
||||
const instruction = await functionCall(client, "inspect_native_instruction", {
|
||||
address: targetAddress,
|
||||
});
|
||||
if (
|
||||
instruction.status !== "decoded" ||
|
||||
!isDefaultMinusOneAssignment(instruction)
|
||||
)
|
||||
throw new Error(
|
||||
`Dense default target lacks the source -1 assignment (x86 register/stack MOV or AArch64 W-register MOVN): ${JSON.stringify(instruction)}`,
|
||||
);
|
||||
return {
|
||||
target_address: targetAddress,
|
||||
instruction_bytes: instruction.bytes,
|
||||
};
|
||||
}
|
||||
|
||||
function isDefaultMinusOneAssignment(instruction) {
|
||||
if (instruction.architecture.startsWith("x86:"))
|
||||
return (
|
||||
instruction.bytes === "b8ffffffff" ||
|
||||
/^c745[0-9a-f]{2}ffffffff$/u.test(instruction.bytes)
|
||||
);
|
||||
if (
|
||||
instruction.architecture.startsWith("AARCH64:") &&
|
||||
instruction.bytes.length === 8
|
||||
) {
|
||||
const word = Buffer.from(instruction.bytes, "hex").readUInt32LE();
|
||||
return (word & 0xffffffe0) === 0x12800000;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
/** Verify numeric/default evidence against independent compiler-byte/source oracles. */
|
||||
export function assertSwitchBoundary(boundary, fixture) {
|
||||
assert.equal(boundary.available, true);
|
||||
assert.equal(boundary.provenance, "ghidra-high-function");
|
||||
assert.ok(
|
||||
boundary.limitations.length > 0,
|
||||
"Provider observation scope must remain explicit",
|
||||
);
|
||||
if (!fixture.table) {
|
||||
assert.deepEqual(
|
||||
boundary.jump_tables,
|
||||
[],
|
||||
"Comparison-only control fabricated a jump table",
|
||||
);
|
||||
return { numeric_cases: 0, default_targets: 0, unknowns: 0 };
|
||||
}
|
||||
assert.equal(
|
||||
boundary.jump_tables.length,
|
||||
1,
|
||||
`Expected one source-owned switch: ${fixture.name}`,
|
||||
);
|
||||
const table = boundary.jump_tables[0];
|
||||
if (fixture.allow_unsigned_32_labels === true) {
|
||||
assert.equal(fixture.discriminator_size_bytes, 4);
|
||||
assert.equal(boundary.parameters.length, 1);
|
||||
assert.equal(boundary.parameters[0].data_type, "undefined4");
|
||||
assert.equal(boundary.parameters[0].size_bytes, 4);
|
||||
assert.equal(boundary.parameters[0].confidence, "low");
|
||||
}
|
||||
assert.equal(
|
||||
table.dispatch_address,
|
||||
fixture.dispatch_address,
|
||||
"Dispatch does not identify the independently byte-proven indirect jump",
|
||||
);
|
||||
const numericCount = assertCaseMappings(table, fixture);
|
||||
assert.equal(
|
||||
table.default_targets.length,
|
||||
1,
|
||||
"Default target must be separate from numeric cases/unknowns",
|
||||
);
|
||||
const defaultTarget = table.default_targets[0];
|
||||
assert.equal(
|
||||
defaultTarget.target_address,
|
||||
fixture.default_address,
|
||||
"Default does not enter the independently byte-proven default block",
|
||||
);
|
||||
assert.equal(defaultTarget.confidence, "high");
|
||||
assert.ok(
|
||||
hasTypedSwitchEvidence(
|
||||
defaultTarget.evidence,
|
||||
"Typed default label",
|
||||
defaultTarget.target_address,
|
||||
table.dispatch_address,
|
||||
),
|
||||
"Default omitted typed label/block/dispatch evidence",
|
||||
);
|
||||
assert.ok(
|
||||
table.data_sources.some(
|
||||
({ address, entry_size_bytes: size, entry_count: count }) =>
|
||||
address === fixture.table_address &&
|
||||
size === 8 &&
|
||||
count === fixture.slots.length,
|
||||
),
|
||||
"Recovered backing-table observation disagrees with independently decoded bytes",
|
||||
);
|
||||
return {
|
||||
numeric_cases: numericCount,
|
||||
default_targets: 1,
|
||||
unknowns: fixture.unsafe_integer ? table.mappings.length : 0,
|
||||
};
|
||||
}
|
||||
|
||||
function assertCaseMappings(table, fixture) {
|
||||
const explicit = fixture.slots.filter(({ explicit_case }) => explicit_case);
|
||||
const numeric = table.mappings.filter(
|
||||
({ case_value }) => typeof case_value === "number",
|
||||
);
|
||||
if (fixture.unsafe_integer) {
|
||||
assert.equal(
|
||||
numeric.length,
|
||||
0,
|
||||
"Unsafe integer labels were rounded, truncated or replaced by table indices",
|
||||
);
|
||||
assert.equal(
|
||||
table.mappings.length,
|
||||
fixture.blocks.length,
|
||||
"Unsafe labels must retain every independently byte-proven target",
|
||||
);
|
||||
assert.deepEqual(
|
||||
table.mappings.map(({ target_address }) => target_address).sort(),
|
||||
fixture.blocks.map(({ address }) => address).sort(),
|
||||
);
|
||||
for (const mapping of table.mappings) {
|
||||
assert.equal(mapping.case_value, null);
|
||||
assert.ok(
|
||||
["low", "medium"].includes(mapping.confidence),
|
||||
"Unknown numeric association must not claim high confidence",
|
||||
);
|
||||
assertUnknownTargetEvidence(
|
||||
mapping.evidence,
|
||||
mapping.target_address,
|
||||
table.dispatch_address,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
assert.equal(
|
||||
table.mappings.length,
|
||||
numeric.length,
|
||||
"Complete public fixture has unresolved/misclassified numeric case rows",
|
||||
);
|
||||
const matchedValues = new Set();
|
||||
for (const mapping of numeric) {
|
||||
const expected = fixture.slots.find(
|
||||
({ value }) =>
|
||||
value === mapping.case_value ||
|
||||
(fixture.allow_unsigned_32_labels === true &&
|
||||
Number(BigInt.asUintN(32, BigInt(value))) === mapping.case_value),
|
||||
);
|
||||
assert.ok(expected, `Fabricated out-of-range case ${mapping.case_value}`);
|
||||
assert.ok(
|
||||
!matchedValues.has(expected.value),
|
||||
"Duplicate discriminator bit pattern",
|
||||
);
|
||||
matchedValues.add(expected.value);
|
||||
assert.equal(
|
||||
mapping.target_address,
|
||||
expected.target_address,
|
||||
`Wrong case→target ${fixture.name}:${mapping.case_value}`,
|
||||
);
|
||||
assert.equal(mapping.confidence, "high");
|
||||
assert.ok(
|
||||
hasTypedSwitchEvidence(
|
||||
mapping.evidence,
|
||||
`Typed case value ${mapping.case_value}`,
|
||||
mapping.target_address,
|
||||
table.dispatch_address,
|
||||
),
|
||||
"Numeric mapping omitted typed case/block/dispatch evidence",
|
||||
);
|
||||
}
|
||||
for (const { value } of explicit)
|
||||
assert.ok(
|
||||
matchedValues.has(value),
|
||||
`Omitted source case ${fixture.name}:${value}`,
|
||||
);
|
||||
}
|
||||
return numeric.length;
|
||||
}
|
||||
|
||||
/** Match typed labels to their explicitly evidenced block and unique dispatch. */
|
||||
export function hasTypedSwitchEvidence(evidence, label, target, dispatch) {
|
||||
return evidence.some(
|
||||
({ kind, source, detail }) =>
|
||||
kind === "jump-table" &&
|
||||
source === "ghidra-clang-case-token" &&
|
||||
typeof detail === "string" &&
|
||||
detail.trim().length > 0 &&
|
||||
detail.startsWith(
|
||||
`${label} belongs to the p-code block starting at recovered target ${target};`,
|
||||
) &&
|
||||
detail.includes(
|
||||
`its unique indirect dispatch predecessor is ${dispatch}.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
function assertUnknownTargetEvidence(evidence, target, dispatch) {
|
||||
assert.ok(
|
||||
evidence.some(
|
||||
({ kind, source, detail }) =>
|
||||
kind === "jump-table" &&
|
||||
source === "ghidra-high-function" &&
|
||||
typeof detail === "string" &&
|
||||
detail.trim().length > 0 &&
|
||||
detail.startsWith(
|
||||
`Ghidra recovered target ${target} at dispatch ${dispatch},`,
|
||||
) &&
|
||||
detail.includes(
|
||||
"no unambiguous, exactly representable case label or default relationship",
|
||||
),
|
||||
),
|
||||
"Unknown mapping omitted recovered target/dispatch and unavailable label evidence",
|
||||
);
|
||||
}
|
||||
|
||||
/** Fault-injected guard controls only; these records are not real provider acceptance. */
|
||||
export function assertSwitchOracleControls(fixtures) {
|
||||
const fixture = fixtures.find(({ name }) => name === "holes_shared");
|
||||
assert.ok(fixture);
|
||||
const boundary = guardBoundary(fixture);
|
||||
const evidence = boundary.jump_tables[0].default_targets[0].evidence;
|
||||
assertSwitchBoundary(boundary, fixture);
|
||||
const faults = {
|
||||
consistently_wrong_dispatch: (table) => {
|
||||
const previous = table.dispatch_address;
|
||||
table.dispatch_address = "0xff";
|
||||
for (const row of [...table.mappings, ...table.default_targets])
|
||||
for (const evidence of row.evidence)
|
||||
evidence.detail = evidence.detail.replace(previous, "0xff");
|
||||
},
|
||||
wrong_numeric_evidence_source: (table) => {
|
||||
table.mappings[0].evidence[0].source = "unrelated-source";
|
||||
},
|
||||
wrong_default_evidence_source: (table) => {
|
||||
table.default_targets[0].evidence[0].source = "ghidra-high-function";
|
||||
},
|
||||
empty_default_evidence_detail: (table) => {
|
||||
table.default_targets[0].evidence[0].detail = "";
|
||||
},
|
||||
wrong_typed_dispatch: (table) => {
|
||||
table.mappings[0].evidence[0].detail =
|
||||
table.mappings[0].evidence[0].detail.replace(
|
||||
`predecessor is ${fixture.dispatch_address}.`,
|
||||
"predecessor is 0xff.",
|
||||
);
|
||||
},
|
||||
missing_default: (table) => {
|
||||
table.default_targets = [];
|
||||
},
|
||||
default_misclassified_as_unknown: (table) => {
|
||||
table.default_targets = [];
|
||||
table.mappings.push({
|
||||
case_value: null,
|
||||
target_address: fixture.default_address,
|
||||
confidence: "low",
|
||||
evidence,
|
||||
});
|
||||
},
|
||||
wrong_shared_target: (table) => {
|
||||
table.mappings.find(({ case_value }) => case_value === 3).target_address =
|
||||
fixture.blocks[0].address;
|
||||
},
|
||||
positional_label_guess: (table) => {
|
||||
table.mappings.find(({ case_value }) => case_value === 9).case_value = 99;
|
||||
},
|
||||
omitted_case: (table) => {
|
||||
table.mappings = table.mappings.filter(
|
||||
({ case_value }) => case_value !== 2,
|
||||
);
|
||||
},
|
||||
wrong_default_target: (table) => {
|
||||
table.default_targets[0].target_address = fixture.blocks[0].address;
|
||||
},
|
||||
wrong_backing_extent: (table) => {
|
||||
table.data_sources[0].entry_count--;
|
||||
},
|
||||
};
|
||||
for (const [name, inject] of Object.entries(faults)) {
|
||||
const damaged = structuredClone(boundary);
|
||||
inject(damaged.jump_tables[0]);
|
||||
assert.throws(
|
||||
() => assertSwitchBoundary(damaged, fixture),
|
||||
undefined,
|
||||
`Oracle failed to reject ${name}`,
|
||||
);
|
||||
}
|
||||
const negative = fixtures.find(({ name }) => name === "negative");
|
||||
assert.ok(negative);
|
||||
const signed = guardBoundary(negative);
|
||||
assertSwitchBoundary(signed, negative);
|
||||
signed.jump_tables[0].mappings = signed.jump_tables[0].mappings.map(
|
||||
(mapping) => ({ ...mapping, case_value: Math.abs(mapping.case_value) }),
|
||||
);
|
||||
assert.throws(
|
||||
() => assertSwitchBoundary(signed, negative),
|
||||
undefined,
|
||||
"Lost unary minus in a signed case label",
|
||||
);
|
||||
const unsignedControls = assertStrippedSignednessControls(negative);
|
||||
const comparison = fixtures.find(({ name }) => name === "comparison");
|
||||
assert.ok(comparison);
|
||||
assertSwitchBoundary({ ...boundary, jump_tables: [] }, comparison);
|
||||
assert.throws(
|
||||
() => assertSwitchBoundary(boundary, comparison),
|
||||
undefined,
|
||||
"Fabricated table for comparison-only control",
|
||||
);
|
||||
return [
|
||||
...assertUnsafeGuardControls(fixtures),
|
||||
...unsignedControls,
|
||||
...Object.keys(faults),
|
||||
"signed_label_magnitude",
|
||||
"fabricated_comparison_table",
|
||||
];
|
||||
}
|
||||
|
||||
function assertStrippedSignednessControls(fixture) {
|
||||
const strippedFixture = { ...fixture, allow_unsigned_32_labels: true };
|
||||
const signed = guardBoundary(fixture);
|
||||
const unsigned = structuredClone(signed);
|
||||
unsigned.parameters = [
|
||||
{ data_type: "undefined4", size_bytes: 4, confidence: "low" },
|
||||
];
|
||||
for (const mapping of unsigned.jump_tables[0].mappings) {
|
||||
const previous = mapping.case_value;
|
||||
mapping.case_value = Number(BigInt.asUintN(32, BigInt(previous)));
|
||||
mapping.evidence[0].detail = mapping.evidence[0].detail.replace(
|
||||
`Typed case value ${previous} `,
|
||||
`Typed case value ${mapping.case_value} `,
|
||||
);
|
||||
}
|
||||
assertSwitchBoundary(unsigned, strippedFixture);
|
||||
assert.throws(
|
||||
() => assertSwitchBoundary(unsigned, fixture),
|
||||
undefined,
|
||||
"Known signed source labels were replaced by unsigned values",
|
||||
);
|
||||
const faults = {
|
||||
stripped_signedness_promoted: (record) => {
|
||||
record.parameters[0].confidence = "high";
|
||||
},
|
||||
stripped_wrong_discriminator_width: (record) => {
|
||||
record.parameters[0].size_bytes = 8;
|
||||
},
|
||||
stripped_wrong_bit_pattern_target: (record) => {
|
||||
record.jump_tables[0].mappings[0].target_address =
|
||||
fixture.blocks[1].address;
|
||||
},
|
||||
stripped_duplicate_bit_pattern: (record) => {
|
||||
record.jump_tables[0].mappings.push(signed.jump_tables[0].mappings[0]);
|
||||
},
|
||||
};
|
||||
for (const [name, inject] of Object.entries(faults)) {
|
||||
const damaged = structuredClone(unsigned);
|
||||
inject(damaged);
|
||||
assert.throws(
|
||||
() => assertSwitchBoundary(damaged, strippedFixture),
|
||||
undefined,
|
||||
`Oracle failed to reject ${name}`,
|
||||
);
|
||||
}
|
||||
return ["known_signed_labels_must_stay_signed", ...Object.keys(faults)];
|
||||
}
|
||||
|
||||
function assertUnsafeGuardControls(fixtures) {
|
||||
const unsafe = fixtures.find(({ name }) => name === "unsafe_integer");
|
||||
assert.ok(unsafe);
|
||||
const unknown = guardBoundary(unsafe, true);
|
||||
assertSwitchBoundary(unknown, unsafe);
|
||||
for (const labels of [
|
||||
unsafe.slots.map(({ value }) => Number(BigInt(value) & 0xffffffffn)),
|
||||
unsafe.slots.map((_, index) => index),
|
||||
unsafe.slots.map(({ value }) => Number(value)),
|
||||
]) {
|
||||
const fabricated = structuredClone(unknown);
|
||||
fabricated.jump_tables[0].mappings = fabricated.jump_tables[0].mappings.map(
|
||||
(mapping, index) => ({
|
||||
...mapping,
|
||||
case_value: labels[index],
|
||||
confidence: "high",
|
||||
}),
|
||||
);
|
||||
assert.throws(
|
||||
() => assertSwitchBoundary(fabricated, unsafe),
|
||||
undefined,
|
||||
"Unsafe label rounding/truncation/index fallback accepted",
|
||||
);
|
||||
}
|
||||
return [
|
||||
"unsafe_label_signed32_truncation",
|
||||
"unsafe_label_index_fallback",
|
||||
"unsafe_label_float_rounding",
|
||||
];
|
||||
}
|
||||
function guardBoundary(fixture, unresolved = false) {
|
||||
const typedEvidence = (label, target) => [
|
||||
{
|
||||
kind: "jump-table",
|
||||
source: "ghidra-clang-case-token",
|
||||
detail: `${label} belongs to the p-code block starting at recovered target ${target}; its unique indirect dispatch predecessor is ${fixture.dispatch_address}.`,
|
||||
},
|
||||
];
|
||||
const unknownEvidence = (target) => [
|
||||
{
|
||||
kind: "jump-table",
|
||||
source: "ghidra-high-function",
|
||||
detail: `Ghidra recovered target ${target} at dispatch ${fixture.dispatch_address}, but no unambiguous, exactly representable case label or default relationship was available.`,
|
||||
},
|
||||
];
|
||||
return {
|
||||
available: true,
|
||||
provenance: "ghidra-high-function",
|
||||
limitations: ["Guard control only"],
|
||||
jump_tables: [
|
||||
{
|
||||
dispatch_address: fixture.dispatch_address,
|
||||
data_sources: [
|
||||
{
|
||||
address: fixture.table_address,
|
||||
entry_size_bytes: 8,
|
||||
entry_count: fixture.slots.length,
|
||||
},
|
||||
],
|
||||
mappings: fixture.slots.map(({ value, target_address }, index) => ({
|
||||
case_value: unresolved ? null : value,
|
||||
target_address,
|
||||
confidence: unresolved
|
||||
? index % 2 === 0
|
||||
? "medium"
|
||||
: "low"
|
||||
: "high",
|
||||
evidence: unresolved
|
||||
? unknownEvidence(target_address)
|
||||
: typedEvidence(`Typed case value ${value}`, target_address),
|
||||
})),
|
||||
default_targets: [
|
||||
{
|
||||
target_address: fixture.default_address,
|
||||
confidence: "high",
|
||||
evidence: typedEvidence(
|
||||
"Typed default label",
|
||||
fixture.default_address,
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, readFile, realpath } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const EXPECTED_CHECKS = [
|
||||
"positive_decimal",
|
||||
"unsigned32_literal_not_rewritten",
|
||||
"negative_embedded_token_sign",
|
||||
"negative_hex",
|
||||
"zero",
|
||||
"magnitude_mismatch_unknown",
|
||||
"signed_encoding_is_not_magnitude",
|
||||
"symbol_label_unknown",
|
||||
"compound_label_unknown",
|
||||
"safe_positive_boundary",
|
||||
"safe_negative_boundary",
|
||||
"safe_hex_boundary",
|
||||
"unsafe_positive_unknown",
|
||||
"unsafe_negative_unknown",
|
||||
"unsigned64_unknown",
|
||||
"no_dispatch_not_attributed",
|
||||
"unique_dispatch_block_start",
|
||||
"instruction_offset_is_not_destination",
|
||||
"wrong_dispatch_not_attributed",
|
||||
"target_not_in_table_not_attributed",
|
||||
"signed_and_positive_distinct",
|
||||
"shared_destination_keeps_all_labels",
|
||||
"default_ordinary_off_is_role",
|
||||
"default_and_numeric_can_share_target",
|
||||
"unsafe64_target_preserved_unknown",
|
||||
"magnitude_mismatch_target_preserved_unknown",
|
||||
"conflicting_value_retracted",
|
||||
"conflicting_destinations_preserved_unknown",
|
||||
"api_first_dispatch_is_insufficient",
|
||||
"multiple_dispatches_rejected",
|
||||
"multiple_dispatch_target_preserved_unknown",
|
||||
"multiple_dispatch_rejected_for_other_table",
|
||||
];
|
||||
|
||||
/** Exercise actual bridge methods on detached Ghidra objects with owned process cleanup. */
|
||||
export async function verifySwitchEvidenceControls({
|
||||
installation,
|
||||
entrypoint,
|
||||
target,
|
||||
workspace,
|
||||
runId,
|
||||
}) {
|
||||
const { spawnOwnedProviderProcess, ProviderProcessSupervisor } =
|
||||
await import("../dist/process/ProviderProcess.js");
|
||||
const { cleanupOwnedProcessGroup } =
|
||||
await import("../dist/process/ProcessOwnership.js");
|
||||
const bridge = await realpath(
|
||||
join(dirname(entrypoint), "../bridge/ghidra/ReaGhidraBridge.java"),
|
||||
);
|
||||
const fixtures = fileURLToPath(
|
||||
new URL("../tests/conformance/ghidra", import.meta.url),
|
||||
);
|
||||
const project = join(workspace, "guard-project");
|
||||
await mkdir(project);
|
||||
const sourceDigest = digest(await readFile(bridge));
|
||||
const targetDigest = digest(await readFile(target));
|
||||
const launch = await spawnOwnedProviderProcess({
|
||||
command: installation.analyzeHeadlessPath,
|
||||
arguments: [
|
||||
project,
|
||||
"SwitchEvidence",
|
||||
"-import",
|
||||
target,
|
||||
"-noanalysis",
|
||||
"-readOnly",
|
||||
"-scriptPath",
|
||||
`${fixtures};${dirname(bridge)}`,
|
||||
"-postScript",
|
||||
"ReaSwitchEvidenceProbe.java",
|
||||
bridge,
|
||||
"-deleteProject",
|
||||
],
|
||||
runId,
|
||||
expectedCommand: null,
|
||||
});
|
||||
const supervisor = new ProviderProcessSupervisor({
|
||||
...launch,
|
||||
ownsProcessLifetime: true,
|
||||
cleanup: () => cleanupOwnedProcessGroup(launch.ownership),
|
||||
});
|
||||
try {
|
||||
assert.ok(
|
||||
await supervisor.waitForExit(120000),
|
||||
"Production switch guard lane exceeded its 120-second headless deadline",
|
||||
);
|
||||
const snapshot = supervisor.snapshot();
|
||||
const output = `${snapshot.stdout.text}\n${snapshot.stderr.text}`;
|
||||
assert.equal(snapshot.exitCode, 0, output);
|
||||
const reports = [
|
||||
...output.matchAll(/REA_SWITCH_EVIDENCE_PROBE_JSON (\{[^\r\n]*\})/gu),
|
||||
];
|
||||
assert.equal(
|
||||
reports.length,
|
||||
1,
|
||||
`Expected one production guard report: ${output}`,
|
||||
);
|
||||
assert.ok(output.includes("REA_SWITCH_EVIDENCE_PROBE_COMPLETE"), output);
|
||||
const report = JSON.parse(reports[0][1]);
|
||||
assertControlReport(
|
||||
report,
|
||||
bridge,
|
||||
sourceDigest,
|
||||
installation.providerVersion,
|
||||
);
|
||||
assert.equal(
|
||||
digest(await readFile(bridge)),
|
||||
sourceDigest,
|
||||
"Guard modified bridge source",
|
||||
);
|
||||
assert.equal(
|
||||
digest(await readFile(target)),
|
||||
targetDigest,
|
||||
"Guard modified imported fixture bytes",
|
||||
);
|
||||
return report;
|
||||
} finally {
|
||||
const stopped = await supervisor.stop();
|
||||
supervisor.dispose();
|
||||
assert.notEqual(stopped.status, "incomplete", JSON.stringify(stopped));
|
||||
}
|
||||
}
|
||||
|
||||
function digest(bytes) {
|
||||
return createHash("sha256").update(bytes).digest("hex");
|
||||
}
|
||||
|
||||
function assertControlReport(report, bridge, sourceDigest, version) {
|
||||
assert.ok(
|
||||
typeof report === "object" && report !== null && !Array.isArray(report),
|
||||
);
|
||||
assert.equal(report.schema_version, 1);
|
||||
assert.equal(report.status, "passed");
|
||||
assert.equal(report.ghidra_version, version);
|
||||
assert.equal(report.bridge_source_path, bridge);
|
||||
assert.equal(report.bridge_source_sha256, sourceDigest);
|
||||
assert.ok(Array.isArray(report.checks));
|
||||
assert.deepEqual(
|
||||
[...report.checks].sort(),
|
||||
[...EXPECTED_CHECKS].sort(),
|
||||
"Production guard did not execute every required case",
|
||||
);
|
||||
assert.equal(report.check_count, EXPECTED_CHECKS.length);
|
||||
assert.ok(
|
||||
typeof report.scope === "string" &&
|
||||
report.scope.includes("detached Ghidra model objects"),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
#!/usr/bin/env node
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
copyFile,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
readdir,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseArgs, promisify } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import {
|
||||
buildSwitchSource,
|
||||
switchSymbolOracle,
|
||||
assertCompiledSwitchOracle,
|
||||
} from "../tests/conformance/ghidra/switch-fixture.mjs";
|
||||
import {
|
||||
assertSwitchBoundary,
|
||||
assertSwitchOracleControls,
|
||||
} from "./verify-real-ghidra-switch-assertions.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { verifySwitchEvidenceControls } from "./verify-real-ghidra-switch-controls.mjs";
|
||||
|
||||
// Narrow compiler-shape lane; no cross compiler or game artifact prerequisites.
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
"compiler-oracle-only": { type: "boolean", default: false },
|
||||
entrypoint: { type: "string" },
|
||||
},
|
||||
});
|
||||
const compilerOnly = values["compiler-oracle-only"];
|
||||
const entrypoint =
|
||||
values.entrypoint === undefined
|
||||
? fileURLToPath(new URL("./rea.mjs", import.meta.url))
|
||||
: resolve(values.entrypoint);
|
||||
assert.ok(
|
||||
process.platform === "linux" && process.arch === "x64",
|
||||
"Ghidra switch compiler lane requires Linux x64; macOS/Windows and cross targets use their separate provider lanes.",
|
||||
);
|
||||
const exec = promisify(execFile);
|
||||
const commands = {
|
||||
gcc: process.env.REA_GCC ?? "gcc",
|
||||
clang: process.env.REA_CLANG ?? "clang",
|
||||
nm: process.env.REA_NM ?? "nm",
|
||||
objdump: process.env.REA_OBJDUMP ?? "objdump",
|
||||
strip: process.env.REA_STRIP ?? "strip",
|
||||
};
|
||||
const versions = {};
|
||||
for (const [role, command] of Object.entries(commands)) {
|
||||
try {
|
||||
const { stdout } = await exec(command, ["--version"]);
|
||||
versions[role] = stdout.split("\n")[0];
|
||||
} catch (cause) {
|
||||
throw new Error(
|
||||
`Ghidra switch compiler lane prerequisite missing: ${role} '${command}'`,
|
||||
{ cause },
|
||||
);
|
||||
}
|
||||
}
|
||||
let installation;
|
||||
if (!compilerOnly) {
|
||||
const { inspectGhidraInstallation } =
|
||||
await import("../dist/ghidra/GhidraInstallation.js");
|
||||
assert.ok(
|
||||
process.env.GHIDRA_INSTALL_DIR,
|
||||
"Ghidra switch lane prerequisite missing: GHIDRA_INSTALL_DIR (bring your own)",
|
||||
);
|
||||
installation = inspectGhidraInstallation({
|
||||
installDir: process.env.GHIDRA_INSTALL_DIR,
|
||||
...(process.env.JAVA_HOME === undefined
|
||||
? {}
|
||||
: { javaHome: process.env.JAVA_HOME }),
|
||||
});
|
||||
assert.equal(installation.status, "available", JSON.stringify(installation));
|
||||
}
|
||||
const run = createVerifierRun();
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-switch-proof-"));
|
||||
const runtime = join(workspace, "runtime");
|
||||
await mkdir(runtime);
|
||||
const source = join(workspace, "switch.c");
|
||||
await writeFile(source, buildSwitchSource());
|
||||
const flags = ["-O2", "-g", "-fno-pie", "-no-pie"];
|
||||
const results = [];
|
||||
let productionControls = null;
|
||||
try {
|
||||
for (const compiler of ["gcc", "clang"]) {
|
||||
const original = join(workspace, `${compiler}-optimized`);
|
||||
await exec(commands[compiler], [...flags, source, "-o", original]);
|
||||
const bytes = await readFile(original);
|
||||
const { stdout: symbols } = await exec(commands.nm, ["-n", original]);
|
||||
const { stdout: disassembly } = await exec(commands.objdump, [
|
||||
"-d",
|
||||
original,
|
||||
]);
|
||||
const fixtures = assertCompiledSwitchOracle(
|
||||
bytes,
|
||||
switchSymbolOracle(symbols),
|
||||
disassembly,
|
||||
);
|
||||
const oracleFaultControls = assertSwitchOracleControls(fixtures);
|
||||
if (!compilerOnly && productionControls === null) {
|
||||
productionControls = await verifySwitchEvidenceControls({
|
||||
installation,
|
||||
entrypoint,
|
||||
target: original,
|
||||
workspace,
|
||||
runId: run.run_id,
|
||||
});
|
||||
assert.equal(
|
||||
digest(await readFile(original)),
|
||||
digest(bytes),
|
||||
"Guard changed compiler-oracled input",
|
||||
);
|
||||
}
|
||||
const stripped = join(workspace, `${compiler}-stripped`);
|
||||
await copyFile(original, stripped);
|
||||
await exec(commands.strip, ["--strip-all", stripped]);
|
||||
// Byte/physical target oracle survives removal of the names that REA could use.
|
||||
assertCompiledSwitchOracle(await readFile(stripped), fixtures, disassembly);
|
||||
for (const [variant, path] of [
|
||||
["optimized", original],
|
||||
["stripped", stripped],
|
||||
]) {
|
||||
const sha256 = digest(await readFile(path));
|
||||
const observation = compilerOnly
|
||||
? null
|
||||
: await verifyTarget(path, sha256, fixtures, variant);
|
||||
assert.equal(
|
||||
digest(await readFile(path)),
|
||||
sha256,
|
||||
"REA analysis changed original compiled fixture",
|
||||
);
|
||||
results.push({
|
||||
compiler,
|
||||
variant,
|
||||
oracle_fault_controls: oracleFaultControls,
|
||||
sha256,
|
||||
compiler_oracle: fixtures.map(
|
||||
({
|
||||
name,
|
||||
address,
|
||||
dispatch_address,
|
||||
table_address,
|
||||
slots,
|
||||
default_address,
|
||||
}) => ({
|
||||
name,
|
||||
address,
|
||||
dispatch_address,
|
||||
table_address,
|
||||
slots,
|
||||
default_address,
|
||||
}),
|
||||
),
|
||||
observation,
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
assert.deepEqual(
|
||||
(await readdir(runtime)).filter((name) => name.startsWith("rea-ghidra-")),
|
||||
[],
|
||||
"Owned Ghidra runtime survived close",
|
||||
);
|
||||
} finally {
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
const lifecycle = await completeVerifierRun(run);
|
||||
if (lifecycle.process_lineage.status === "verified")
|
||||
assert.deepEqual(lifecycle.process_lineage.descendants, []);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ lane: "linux-x64-compiler-switch", compiler_only: compilerOnly, entrypoint, versions, compiler_flags: flags, verifier_run: lifecycle, production_controls: productionControls, results, limitations: ["Exact compiler oracle admits absolute 8-byte native x86-64 ELF tables and fails explicitly when a compiler changes lowering.", "Compiled binaries are analyzed read-only; target programs are never executed.", "Comparison-only control proves absence of a fabricated table and preservation of provider limitations, not recovery of C source.", "Detached Ghidra object controls exercise actual bridge methods through reflection; they do not claim naturally compiled ambiguous dispatches.", "CLI/MCP comparison covers switch observations, excluding process-specific Evidence IDs."], cleanup: "complete" })}\n`,
|
||||
);
|
||||
|
||||
function digest(bytes) {
|
||||
return createHash("sha256").update(bytes).digest("hex");
|
||||
}
|
||||
async function verifyTarget(path, sha256, fixtures, variant) {
|
||||
const env = {
|
||||
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
||||
TMPDIR: runtime,
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_ANALYSIS_PROVIDER: "ghidra",
|
||||
REA_PROCESS_RUN_ID: run.run_id,
|
||||
GHIDRA_INSTALL_DIR: process.env.GHIDRA_INSTALL_DIR,
|
||||
...(process.env.JAVA_HOME === undefined
|
||||
? {}
|
||||
: { JAVA_HOME: process.env.JAVA_HOME }),
|
||||
};
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [entrypoint, "mcp"],
|
||||
env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({ name: "rea-real-switch-proof", version: "1" });
|
||||
let opened = false;
|
||||
let stderr = "";
|
||||
const observations = [];
|
||||
try {
|
||||
await client.connect(transport);
|
||||
transport.stderr?.on("data", (chunk) => {
|
||||
stderr = (stderr + chunk.toString()).slice(-65536);
|
||||
});
|
||||
const target = await call(client, "open_binary", {
|
||||
path,
|
||||
provider: "ghidra",
|
||||
});
|
||||
opened = true;
|
||||
assert.equal(target.sha256, sha256);
|
||||
assert.equal(target.architecture, "x86_64");
|
||||
assert.equal(target.format, "elf");
|
||||
const procedures = await call(client, "list_procedures");
|
||||
for (const fixture of fixtures) {
|
||||
const observationFixture = {
|
||||
...fixture,
|
||||
allow_unsigned_32_labels:
|
||||
variant === "stripped" && fixture.name === "negative",
|
||||
};
|
||||
const procedure = procedures.find(
|
||||
({ address }) => address === fixture.address,
|
||||
);
|
||||
assert.ok(
|
||||
procedure,
|
||||
`Source-owned function at ${fixture.address} not discovered in optimized/stripped fixture`,
|
||||
);
|
||||
const observed = await toolResult(client, "inspect_native_api", {
|
||||
procedure: procedure.address,
|
||||
});
|
||||
assertWorkflowIdentity(observed.evidence, sha256);
|
||||
assert.equal(observed.result.procedure.address, procedure.address);
|
||||
const summary = assertSwitchBoundary(
|
||||
observed.result.boundary,
|
||||
observationFixture,
|
||||
);
|
||||
assert.deepEqual(
|
||||
[...observed.result.residual_unknowns].sort(),
|
||||
expectedResidualUnknowns(observed.result.boundary, fixture).sort(),
|
||||
"Residual unknowns must preserve unresolved ABI types and exactly the expected switch uncertainty",
|
||||
);
|
||||
const cli = await cliSwitchEvidence(
|
||||
entrypoint,
|
||||
path,
|
||||
procedure.address,
|
||||
env,
|
||||
);
|
||||
assertWorkflowIdentity(cli, sha256);
|
||||
assert.equal(cli.normalized_result.procedure.address, procedure.address);
|
||||
assertSwitchBoundary(cli.normalized_result.boundary, observationFixture);
|
||||
assert.deepEqual(
|
||||
cli.normalized_result.boundary.jump_tables,
|
||||
observed.result.boundary.jump_tables,
|
||||
"CLI/MCP switch mappings/defaults/data-source evidence differ",
|
||||
);
|
||||
assert.deepEqual(
|
||||
cli.normalized_result.residual_unknowns,
|
||||
observed.result.residual_unknowns,
|
||||
);
|
||||
observations.push({
|
||||
name: fixture.name,
|
||||
...summary,
|
||||
cli_mcp_equal: true,
|
||||
limitations: observed.result.boundary.limitations,
|
||||
residual_unknowns: observed.result.residual_unknowns,
|
||||
jump_tables: observed.result.boundary.jump_tables,
|
||||
});
|
||||
}
|
||||
return { observations, stderr };
|
||||
} finally {
|
||||
try {
|
||||
if (opened) await call(client, "close_binary");
|
||||
} finally {
|
||||
try {
|
||||
await client.close();
|
||||
} finally {
|
||||
await transport.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function assertWorkflowIdentity(evidence, sha256) {
|
||||
assert.equal(evidence.subject.digest.sha256, sha256);
|
||||
assert.equal(evidence.operation, "inspect_native_api");
|
||||
assert.equal(evidence.provider.id, "rea-workflow");
|
||||
assert.equal(evidence.analysis_profile.provider.id, "rea-workflow");
|
||||
assert.equal(
|
||||
evidence.analysis_profile.parameters.upstream_analysis_profile.parameters
|
||||
.language_id,
|
||||
"auto-from-header",
|
||||
);
|
||||
}
|
||||
|
||||
function expectedResidualUnknowns(boundary, fixture) {
|
||||
const unknowns = [boundary.return_type, ...boundary.parameters]
|
||||
.filter(({ confidence }) => confidence === "low")
|
||||
.map(
|
||||
({ role, data_type: type }) =>
|
||||
`Can the inferred ${role} type ${type} be confirmed with imported metadata or an ABI probe?`,
|
||||
);
|
||||
if (fixture.unsafe_integer)
|
||||
unknowns.push(
|
||||
`Which source-level case values correspond to every target dispatched at ${boundary.jump_tables[0].dispatch_address}?`,
|
||||
);
|
||||
return [...new Set(unknowns)];
|
||||
}
|
||||
async function toolResult(client, name, arguments_ = {}) {
|
||||
const result = await client.callTool(
|
||||
{ name, arguments: arguments_ },
|
||||
{ timeout: 240000 },
|
||||
);
|
||||
assert.notEqual(result.isError, true, JSON.stringify(result));
|
||||
assert.ok(
|
||||
result.structuredContent && "result" in result.structuredContent,
|
||||
`${name} omitted structured result`,
|
||||
);
|
||||
return result.structuredContent;
|
||||
}
|
||||
async function call(client, name, arguments_ = {}) {
|
||||
return (await toolResult(client, name, arguments_)).result;
|
||||
}
|
||||
|
||||
async function cliSwitchEvidence(entrypoint, path, procedure, env) {
|
||||
const { stdout } = await exec(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"inspect-native-api",
|
||||
path,
|
||||
procedure,
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
],
|
||||
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
return JSON.parse(stdout);
|
||||
}
|
||||
@@ -33,8 +33,10 @@ import {
|
||||
verifyInventoryOperations,
|
||||
} from "./verify-real-ghidra-inventory.mjs";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { hasTypedSwitchEvidence } from "./verify-real-ghidra-switch-assertions.mjs";
|
||||
|
||||
import {
|
||||
verifyDenseDefaultReturn,
|
||||
verifyNativeTypeLayout,
|
||||
verifyRelativeSwitch,
|
||||
verifyNativeValueTrace,
|
||||
@@ -308,8 +310,7 @@ function nativeFixtureTarget(platform, architecture) {
|
||||
async function verifyNativeApiCli(
|
||||
targetPath,
|
||||
procedures,
|
||||
requireDenseJumpTable,
|
||||
denseSwitchSymbol = "rea_ghidra_inventory_dense_switch",
|
||||
{ requireDenseJumpTable, denseSwitchSymbol, client },
|
||||
) {
|
||||
const denseSwitch = procedures.find(({ value }) =>
|
||||
value.endsWith(denseSwitchSymbol),
|
||||
@@ -349,11 +350,25 @@ async function verifyNativeApiCli(
|
||||
(mapping) =>
|
||||
mapping.case_value === caseValue &&
|
||||
mapping.confidence === "high" &&
|
||||
mapping.evidence.some(
|
||||
({ detail }) =>
|
||||
detail.includes(`case value ${caseValue} with target`) ||
|
||||
detail.startsWith(`Case value ${caseValue} indexes byte `),
|
||||
),
|
||||
(hasTypedSwitchEvidence(
|
||||
mapping.evidence,
|
||||
`Typed case value ${caseValue}`,
|
||||
mapping.target_address,
|
||||
denseTable.dispatch_address,
|
||||
) ||
|
||||
mapping.evidence.some(
|
||||
({ kind, source, detail }) =>
|
||||
kind === "jump-table" &&
|
||||
typeof detail === "string" &&
|
||||
detail.trim().length > 0 &&
|
||||
((source === "ghidra-high-function" &&
|
||||
detail.includes(
|
||||
`case value ${caseValue} with target ${mapping.target_address}`,
|
||||
)) ||
|
||||
(source === "ghidra-instruction-and-memory" &&
|
||||
detail.startsWith(`Case value ${caseValue} indexes byte `) &&
|
||||
detail.includes(`reaching ${mapping.target_address}.`))),
|
||||
)),
|
||||
),
|
||||
);
|
||||
if (
|
||||
@@ -368,7 +383,20 @@ async function verifyNativeApiCli(
|
||||
if (
|
||||
requireDenseJumpTable &&
|
||||
(denseTable === undefined ||
|
||||
denseCases.size !== denseTable.mappings.length ||
|
||||
denseCases.size !== expectedCases.length ||
|
||||
denseTable.mappings.length !== expectedCases.length ||
|
||||
denseTable.default_targets.length !== 1 ||
|
||||
denseTable.default_targets[0].confidence !== "high" ||
|
||||
!hasTypedSwitchEvidence(
|
||||
denseTable.default_targets[0].evidence,
|
||||
"Typed default label",
|
||||
denseTable.default_targets[0].target_address,
|
||||
denseTable.dispatch_address,
|
||||
) ||
|
||||
denseTable.mappings.some(
|
||||
({ target_address }) =>
|
||||
target_address === denseTable.default_targets[0].target_address,
|
||||
) ||
|
||||
expectedCases.some((caseValue) => !denseCases.has(caseValue)) ||
|
||||
!mappingsAreExact ||
|
||||
evidence.normalized_result?.residual_unknowns?.length !== 0)
|
||||
@@ -376,10 +404,17 @@ async function verifyNativeApiCli(
|
||||
throw new Error(
|
||||
`The shipped inspect-native-api CLI did not preserve complete dense jump-table output: ${stdout}`,
|
||||
);
|
||||
const defaultOracle = requireDenseJumpTable
|
||||
? await verifyDenseDefaultReturn(
|
||||
client,
|
||||
denseTable.default_targets[0].target_address,
|
||||
)
|
||||
: null;
|
||||
return {
|
||||
operation: evidence.operation,
|
||||
provider: evidence.provider,
|
||||
mappings_returned: denseTable?.mappings.length ?? null,
|
||||
default_oracle: defaultOracle,
|
||||
residual_unknowns: evidence.normalized_result.residual_unknowns.length,
|
||||
};
|
||||
}
|
||||
@@ -456,16 +491,16 @@ async function verifyTarget(targetPath, variant, expectedTarget = null) {
|
||||
variant === "cross-arm64-elf" || variant === "aarch64-jump-table";
|
||||
const nativeApiCli =
|
||||
variant === "debug" || crossArm64Elf
|
||||
? await verifyNativeApiCli(
|
||||
parsedTarget.value.path,
|
||||
procedures,
|
||||
crossArm64Elf ||
|
||||
? await verifyNativeApiCli(parsedTarget.value.path, procedures, {
|
||||
requireDenseJumpTable:
|
||||
crossArm64Elf ||
|
||||
(expectedNativeTarget.format === "elf" &&
|
||||
expectedNativeTarget.architecture === "x86_64"),
|
||||
crossArm64Elf
|
||||
denseSwitchSymbol: crossArm64Elf
|
||||
? "rea_cross_dense_switch"
|
||||
: "rea_ghidra_inventory_dense_switch",
|
||||
)
|
||||
client,
|
||||
})
|
||||
: null;
|
||||
const probes = variant.startsWith("relative-")
|
||||
? await verifyRelativeSwitch(
|
||||
|
||||
@@ -1,356 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFile, realpath } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
import { PermissionAuthority } from "../dist/application/PermissionAuthority.js";
|
||||
import { runControlledReplay } from "../dist/application/JavaScriptReplayService.js";
|
||||
import { createPermissionPolicy } from "../dist/domain/permissionPolicy.js";
|
||||
import { LinuxJavaScriptReplayRunner } from "../dist/replay/LinuxJavaScriptReplayRunner.js";
|
||||
import { SystemJavaScriptReplayHost } from "../dist/replay/SystemJavaScriptReplayHost.js";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
|
||||
const verifierRun = createVerifierRun();
|
||||
|
||||
const suppliedInputPath = process.env.REA_REPLAY_INPUT_PATH;
|
||||
const suppliedInput =
|
||||
suppliedInputPath === undefined
|
||||
? undefined
|
||||
: JSON.parse(await readFile(await realpath(suppliedInputPath), "utf8"));
|
||||
const canonicalSide = async (side) => ({
|
||||
...side,
|
||||
modules: await Promise.all(
|
||||
side.modules.map(async (module) => ({
|
||||
...module,
|
||||
path: await realpath(module.path),
|
||||
})),
|
||||
),
|
||||
});
|
||||
const leftPath = await realpath(
|
||||
process.env.REA_REPLAY_LEFT_PATH ??
|
||||
resolve("tests/fixtures/replay/parser.mjs"),
|
||||
);
|
||||
const rightPath = await realpath(
|
||||
process.env.REA_REPLAY_RIGHT_PATH ??
|
||||
resolve("tests/fixtures/replay/parser-v2.mjs"),
|
||||
);
|
||||
const suppliedLeft =
|
||||
suppliedInput === undefined
|
||||
? undefined
|
||||
: await canonicalSide(suppliedInput.left);
|
||||
const suppliedRight =
|
||||
suppliedInput?.right === undefined
|
||||
? undefined
|
||||
: await canonicalSide(suppliedInput.right);
|
||||
const roots = [
|
||||
...new Set(
|
||||
suppliedLeft === undefined
|
||||
? [dirname(leftPath), dirname(rightPath)]
|
||||
: [...suppliedLeft.modules, ...(suppliedRight?.modules ?? [])].map(
|
||||
({ path }) => dirname(path),
|
||||
),
|
||||
),
|
||||
];
|
||||
const policy = {
|
||||
status: "enabled",
|
||||
roots,
|
||||
nodePath: process.execPath,
|
||||
bubblewrapPath:
|
||||
process.env.REA_JAVASCRIPT_REPLAY_BWRAP_PATH ?? "/usr/bin/bwrap",
|
||||
systemdRunPath:
|
||||
process.env.REA_JAVASCRIPT_REPLAY_SYSTEMD_RUN_PATH ??
|
||||
"/usr/bin/systemd-run",
|
||||
systemctlPath:
|
||||
process.env.REA_JAVASCRIPT_REPLAY_SYSTEMCTL_PATH ?? "/usr/bin/systemctl",
|
||||
shellPath: process.env.REA_JAVASCRIPT_REPLAY_SHELL_PATH ?? "/usr/bin/bash",
|
||||
};
|
||||
const ceiling = {
|
||||
capability: "javascript_replay",
|
||||
roots,
|
||||
executables: [
|
||||
policy.nodePath,
|
||||
policy.bubblewrapPath,
|
||||
policy.systemdRunPath,
|
||||
policy.systemctlPath,
|
||||
policy.shellPath,
|
||||
],
|
||||
environment_names: [],
|
||||
network: "none",
|
||||
mount: true,
|
||||
};
|
||||
const authority = new PermissionAuthority(
|
||||
createPermissionPolicy(
|
||||
[ceiling],
|
||||
[
|
||||
{
|
||||
...ceiling,
|
||||
grant_id: "administrator:javascript_replay",
|
||||
lifetime: "administrator",
|
||||
operation_identity: null,
|
||||
expires_at: null,
|
||||
},
|
||||
],
|
||||
),
|
||||
);
|
||||
const dependencies = {
|
||||
policy: () => policy,
|
||||
host: new SystemJavaScriptReplayHost(),
|
||||
runner: new LinuxJavaScriptReplayRunner(),
|
||||
authority,
|
||||
};
|
||||
const defaultInput = {
|
||||
mode: "plan",
|
||||
left: {
|
||||
modules: [
|
||||
{
|
||||
alias: "parser",
|
||||
path: leftPath,
|
||||
format: "esm",
|
||||
role: "module",
|
||||
dependencies: {},
|
||||
},
|
||||
],
|
||||
entry_alias: "parser",
|
||||
entry_export: "default",
|
||||
},
|
||||
right: {
|
||||
modules: [
|
||||
{
|
||||
alias: "parser",
|
||||
path: rightPath,
|
||||
format: "esm",
|
||||
role: "module",
|
||||
dependencies: {},
|
||||
},
|
||||
],
|
||||
entry_alias: "parser",
|
||||
entry_export: "default",
|
||||
},
|
||||
cases: [
|
||||
{ case_id: "heading", arguments: ["# Title"] },
|
||||
{ case_id: "plain", arguments: ["plain"] },
|
||||
],
|
||||
generator: { preset: "parser-boundaries", seed: 7, count: 4 },
|
||||
};
|
||||
const suppliedPlanInput =
|
||||
suppliedInput === undefined
|
||||
? undefined
|
||||
: (({ approved: _approved, plan_digest: _planDigest, ...planInput }) =>
|
||||
planInput)(suppliedInput);
|
||||
const input =
|
||||
suppliedPlanInput === undefined
|
||||
? defaultInput
|
||||
: {
|
||||
...suppliedPlanInput,
|
||||
mode: "plan",
|
||||
left: suppliedLeft,
|
||||
...(suppliedRight === undefined ? {} : { right: suppliedRight }),
|
||||
reproducer_export: undefined,
|
||||
};
|
||||
|
||||
const planned = await runControlledReplay(dependencies, input);
|
||||
if (!planned.ok) {
|
||||
if (process.env.REA_REPLAY_ALLOW_UNAVAILABLE === "true") {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), available: false, error_tag: planned.error._tag })}\n`,
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
throw planned.error;
|
||||
}
|
||||
const plan = planned.value.plan;
|
||||
if (plan === null || typeof plan.plan_digest !== "string")
|
||||
throw new Error("Real replay verifier did not receive a plan");
|
||||
const executed = await runControlledReplay(dependencies, {
|
||||
...input,
|
||||
mode: "execute",
|
||||
plan_digest: plan.plan_digest,
|
||||
});
|
||||
if (!executed.ok) throw executed.error;
|
||||
const evidence = executed.value.evidence;
|
||||
if (
|
||||
evidence === null ||
|
||||
evidence.authority !== "controlled-replay" ||
|
||||
evidence.environment?.isolation !== "container" ||
|
||||
evidence.normalized_result.termination !== "completed" ||
|
||||
evidence.normalized_result.cleanup.state !== "complete" ||
|
||||
evidence.normalized_result.comparison?.some(
|
||||
({ status }) => status === "unknown",
|
||||
) === true
|
||||
)
|
||||
throw new Error(
|
||||
`Real replay verifier received incomplete Evidence: ${JSON.stringify({
|
||||
authority: evidence?.authority ?? null,
|
||||
isolation: evidence?.environment?.isolation ?? null,
|
||||
termination: evidence?.normalized_result?.termination ?? null,
|
||||
cleanup: evidence?.normalized_result?.cleanup?.state ?? null,
|
||||
outcomes:
|
||||
evidence?.normalized_result?.outcomes?.map(({ outcome }) => outcome) ??
|
||||
[],
|
||||
stderr: evidence?.normalized_result?.stderr ?? null,
|
||||
comparison_statuses:
|
||||
evidence?.normalized_result?.comparison?.map(({ status }) => status) ??
|
||||
[],
|
||||
})}`,
|
||||
);
|
||||
|
||||
if (suppliedInput !== undefined) {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
verifier_run: await completeVerifierRun(verifierRun),
|
||||
available: true,
|
||||
supplied_manifest: true,
|
||||
plan_digest: plan.plan_digest,
|
||||
evidence_id: evidence.evidence_id,
|
||||
source_evidence_ids: executed.value.source_evidence.map(
|
||||
({ evidence_id: id }) => id,
|
||||
),
|
||||
left_sha256: plan.left.modules.map(({ sha256 }) => sha256),
|
||||
right_sha256: plan.right?.modules.map(({ sha256 }) => sha256) ?? [],
|
||||
cases: plan.cases.length,
|
||||
comparison:
|
||||
evidence.normalized_result.comparison?.map(({ case_id, status }) => ({
|
||||
case_id,
|
||||
status,
|
||||
})) ?? [],
|
||||
cleanup: evidence.normalized_result.cleanup.state,
|
||||
})}\n`,
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const runSingle = async (path, limits = undefined) => {
|
||||
const manifest = {
|
||||
mode: "plan",
|
||||
left: {
|
||||
modules: [
|
||||
{
|
||||
alias: "entry",
|
||||
path: await realpath(path),
|
||||
format: "esm",
|
||||
role: "module",
|
||||
dependencies: {},
|
||||
},
|
||||
],
|
||||
entry_alias: "entry",
|
||||
entry_export: "default",
|
||||
},
|
||||
cases: [{ case_id: "probe", arguments: [] }],
|
||||
...(limits === undefined ? {} : { limits }),
|
||||
};
|
||||
const proposed = await runControlledReplay(dependencies, manifest);
|
||||
if (!proposed.ok || proposed.value.plan === null)
|
||||
throw new Error("Replay conformance plan failed");
|
||||
const observed = await runControlledReplay(dependencies, {
|
||||
...manifest,
|
||||
mode: "execute",
|
||||
plan_digest: proposed.value.plan.plan_digest,
|
||||
});
|
||||
if (!observed.ok || observed.value.evidence === null)
|
||||
throw new Error("Replay conformance execution failed");
|
||||
return observed.value.evidence.normalized_result;
|
||||
};
|
||||
|
||||
const sideEffects = await runSingle(
|
||||
resolve("tests/fixtures/replay/side-effect-attempt.mjs"),
|
||||
);
|
||||
if (
|
||||
sideEffects.outcomes[0]?.outcome !== "return" ||
|
||||
Object.values(sideEffects.outcomes[0].value).some(
|
||||
(value) => value !== "undefined",
|
||||
)
|
||||
)
|
||||
throw new Error("Ambient side-effect surface was exposed to replay code");
|
||||
|
||||
const timeout = await runSingle(
|
||||
resolve("tests/fixtures/replay/busy-loop.mjs"),
|
||||
{
|
||||
wall_time_ms: 100,
|
||||
},
|
||||
);
|
||||
if (timeout.termination !== "timeout" || timeout.cleanup.state !== "complete")
|
||||
throw new Error("Replay timeout did not kill and clean the complete sandbox");
|
||||
|
||||
const differentialTimeoutInput = {
|
||||
mode: "plan",
|
||||
left: {
|
||||
modules: [
|
||||
{
|
||||
alias: "entry",
|
||||
path: await realpath(resolve("tests/fixtures/replay/busy-loop.mjs")),
|
||||
format: "esm",
|
||||
role: "module",
|
||||
dependencies: {},
|
||||
},
|
||||
],
|
||||
entry_alias: "entry",
|
||||
entry_export: "default",
|
||||
},
|
||||
right: {
|
||||
modules: [
|
||||
{
|
||||
alias: "entry",
|
||||
path: await realpath(resolve("tests/fixtures/replay/parser.mjs")),
|
||||
format: "esm",
|
||||
role: "module",
|
||||
dependencies: {},
|
||||
},
|
||||
],
|
||||
entry_alias: "entry",
|
||||
entry_export: "default",
|
||||
},
|
||||
cases: [{ case_id: "probe", arguments: [] }],
|
||||
limits: { wall_time_ms: 100 },
|
||||
};
|
||||
const differentialTimeoutPlan = await runControlledReplay(
|
||||
dependencies,
|
||||
differentialTimeoutInput,
|
||||
);
|
||||
if (!differentialTimeoutPlan.ok || differentialTimeoutPlan.value.plan === null)
|
||||
throw new Error("Differential timeout plan failed");
|
||||
const differentialTimeout = await runControlledReplay(dependencies, {
|
||||
...differentialTimeoutInput,
|
||||
mode: "execute",
|
||||
plan_digest: differentialTimeoutPlan.value.plan.plan_digest,
|
||||
});
|
||||
if (
|
||||
!differentialTimeout.ok ||
|
||||
differentialTimeout.value.evidence === null ||
|
||||
differentialTimeout.value.source_evidence.length !== 2 ||
|
||||
differentialTimeout.value.evidence.normalized_result.comparison?.[0]
|
||||
?.status !== "unknown" ||
|
||||
differentialTimeout.value.source_evidence.some(
|
||||
(item) => item.normalized_result.outcomes[0]?.outcome !== "timeout",
|
||||
)
|
||||
)
|
||||
throw new Error("Differential termination did not retain both observations");
|
||||
|
||||
const memory = await runSingle(
|
||||
resolve("tests/fixtures/replay/memory-pressure.mjs"),
|
||||
{ wall_time_ms: 3000, memory_bytes: 32 * 1024 * 1024 },
|
||||
);
|
||||
if (memory.termination !== "oom" || memory.cleanup.state !== "complete")
|
||||
throw new Error(
|
||||
`Replay OOM did not report and clean the cgroup limit: ${JSON.stringify(memory)}`,
|
||||
);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
verifier_run: await completeVerifierRun(verifierRun),
|
||||
available: true,
|
||||
plan_digest: plan.plan_digest,
|
||||
evidence_id: evidence.evidence_id,
|
||||
left_sha256: plan.left.modules[0]?.sha256,
|
||||
right_sha256: plan.right?.modules[0]?.sha256,
|
||||
cases: plan.cases.length,
|
||||
comparison: evidence.normalized_result.comparison?.map(
|
||||
({ case_id, status }) => ({ case_id, status }),
|
||||
),
|
||||
cleanup: evidence.normalized_result.cleanup.state,
|
||||
side_effect_surface: "unavailable",
|
||||
timeout: timeout.termination,
|
||||
differential_timeout:
|
||||
differentialTimeout.value.evidence.normalized_result.comparison[0]
|
||||
?.status,
|
||||
memory: memory.termination,
|
||||
})}\n`,
|
||||
);
|
||||
@@ -1,82 +1,179 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import { once } from "node:events";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
const root = resolve(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
"../tests/conformance/runtime-inspector",
|
||||
);
|
||||
const targetPath = resolve(root, "node-target.mjs");
|
||||
const child = spawn(process.execPath, ["--inspect=127.0.0.1:0", targetPath], {
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const root = await realpath(await mkdtemp(join(tmpdir(), "rea-inspector-")));
|
||||
const environment = {
|
||||
...process.env,
|
||||
};
|
||||
const client = new Client({
|
||||
name: "rea-inspector-conformance",
|
||||
version: "1.0.0",
|
||||
});
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: ["scripts/rea.mjs", "mcp"],
|
||||
cwd: repository,
|
||||
env: environment,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const names = [
|
||||
"node-target.mjs",
|
||||
"plain.cjs",
|
||||
"with space.cjs",
|
||||
"with_under_score.cjs",
|
||||
"with#hash.cjs",
|
||||
"with%percent.cjs",
|
||||
"with%23hash.cjs",
|
||||
...(process.platform === "win32" ? [] : ['with"quote.cjs']),
|
||||
];
|
||||
|
||||
try {
|
||||
const webSocketUrl = await inspectorUrl(child);
|
||||
const socket = new URL(webSocketUrl);
|
||||
await client.connect(transport);
|
||||
for (const name of names) {
|
||||
const targetPath =
|
||||
name === "node-target.mjs"
|
||||
? await realpath(
|
||||
join(repository, "tests/conformance/runtime-inspector", name),
|
||||
)
|
||||
: join(root, name);
|
||||
if (name !== "node-target.mjs")
|
||||
await writeFile(targetPath, "setInterval(() => {}, 1000);\n");
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
["--inspect=127.0.0.1:0", targetPath],
|
||||
{
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
},
|
||||
);
|
||||
try {
|
||||
await verifyTarget(child, targetPath, name);
|
||||
} finally {
|
||||
await terminate(child);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
await client.close();
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyTarget(child, targetPath, name) {
|
||||
const socket = new URL(await inspectorUrl(child));
|
||||
const endpoint = `http://127.0.0.1:${socket.port}`;
|
||||
const environment = {
|
||||
...process.env,
|
||||
REA_V8_INSPECTOR_OBSERVE_ENABLED: "true",
|
||||
};
|
||||
const listed = await runCli(
|
||||
["list-javascript-runtime-targets", endpoint, "--json"],
|
||||
environment,
|
||||
);
|
||||
const target = listed.normalized_result?.targets?.[0];
|
||||
if (
|
||||
listed.operation !== "list_javascript_runtime_targets" ||
|
||||
typeof target?.target_id !== "string" ||
|
||||
target.location?.kind !== "file" ||
|
||||
target.location.file_path !== targetPath
|
||||
)
|
||||
throw new Error("Real Node Inspector target discovery did not match");
|
||||
const observed = await runCli(
|
||||
[
|
||||
"observe-javascript-runtime",
|
||||
endpoint,
|
||||
target.target_id,
|
||||
"--runtime-kind",
|
||||
"node",
|
||||
"--observation-ms",
|
||||
"100",
|
||||
"--json",
|
||||
],
|
||||
environment,
|
||||
);
|
||||
const result = observed.normalized_result;
|
||||
if (
|
||||
observed.operation !== "observe_javascript_runtime" ||
|
||||
observed.provider?.id !== "rea-v8-inspector" ||
|
||||
result?.target?.target_id !== target.target_id ||
|
||||
result?.target?.runtime_kind !== "node" ||
|
||||
!Array.isArray(result?.scripts?.items) ||
|
||||
result.scripts.items.length === 0
|
||||
)
|
||||
throw new Error("Real Node Inspector observation did not match");
|
||||
if (
|
||||
!Array.isArray(result.directly_observed) ||
|
||||
!Array.isArray(result.unavailable_without_instrumentation) ||
|
||||
result.capture?.truncated !== false
|
||||
)
|
||||
throw new Error("Real Node Inspector capture was incomplete");
|
||||
const response = await fetch(`${endpoint}/json/list`);
|
||||
assert.equal(response.ok, true);
|
||||
const discovery = await response.json();
|
||||
assert.equal(discovery.length, 1);
|
||||
const raw = discovery[0];
|
||||
assert.equal(typeof raw.id, "string");
|
||||
// Independent oracle: the fixture owns the pathname, and Node's discovery
|
||||
// serialization loses quotes/backslashes. Never decode underscores into it.
|
||||
const unresolved = process.platform === "win32" || /[_"\\]/u.test(targetPath);
|
||||
const expectedLocation = unresolved
|
||||
? {
|
||||
kind: "unresolved",
|
||||
reported_url: raw.url,
|
||||
reason: "unverifiable-file-location",
|
||||
}
|
||||
: { kind: "file", file_path: targetPath };
|
||||
const listed = await runCli([
|
||||
"list-javascript-runtime-targets",
|
||||
endpoint,
|
||||
"--json",
|
||||
]);
|
||||
assert.equal(listed.operation, "list_javascript_runtime_targets");
|
||||
const inventory = listed.normalized_result;
|
||||
assert.equal(inventory.targets.length, 1);
|
||||
assert.equal(inventory.targets[0].target_id, raw.id);
|
||||
assert.deepEqual(inventory.targets[0].location, expectedLocation);
|
||||
assert.equal(inventory.excluded.unsupported_location, 0);
|
||||
const mcpList = await runMcp("list_javascript_runtime_targets", {
|
||||
inspector_endpoint: endpoint,
|
||||
});
|
||||
assert.deepEqual(mcpList, inventory);
|
||||
|
||||
const observed = await runCli([
|
||||
"observe-javascript-runtime",
|
||||
endpoint,
|
||||
raw.id,
|
||||
"--runtime-kind",
|
||||
"node",
|
||||
"--observation-ms",
|
||||
"100",
|
||||
"--json",
|
||||
]);
|
||||
assert.equal(observed.operation, "observe_javascript_runtime");
|
||||
assert.equal(observed.provider.id, "rea-v8-inspector");
|
||||
const mcpObservation = await runMcp("observe_javascript_runtime", {
|
||||
inspector_endpoint: endpoint,
|
||||
target_id: raw.id,
|
||||
runtime_kind: "node",
|
||||
observation_ms: 100,
|
||||
});
|
||||
for (const result of [observed.normalized_result, mcpObservation])
|
||||
verifyObservation(result, raw.id, expectedLocation, targetPath);
|
||||
assert.deepEqual(mcpObservation.target, observed.normalized_result.target);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
status: "pass",
|
||||
provider: observed.provider.id,
|
||||
target_id: target.target_id,
|
||||
platform: process.platform,
|
||||
node: process.version,
|
||||
fixture: name,
|
||||
target_id: raw.id,
|
||||
discovery_url: raw.url,
|
||||
target_location: expectedLocation,
|
||||
cli_scripts: observed.normalized_result.scripts.items.length,
|
||||
mcp_scripts: mcpObservation.scripts.items.length,
|
||||
listed_evidence_id: listed.evidence_id,
|
||||
observation_evidence_id: observed.evidence_id,
|
||||
scripts: result.scripts.items.length,
|
||||
contexts: result.execution_contexts.length,
|
||||
endpoint,
|
||||
})}\n`,
|
||||
);
|
||||
} finally {
|
||||
await terminate(child);
|
||||
}
|
||||
|
||||
function verifyObservation(result, targetId, expectedLocation, targetPath) {
|
||||
assert.equal(result.target.target_id, targetId);
|
||||
assert.equal(result.target.runtime_kind, "node");
|
||||
assert.deepEqual(result.target.location, expectedLocation);
|
||||
assert.equal(result.capture.truncated, false);
|
||||
assert.ok(Array.isArray(result.directly_observed));
|
||||
assert.ok(Array.isArray(result.unavailable_without_instrumentation));
|
||||
assert.ok(
|
||||
result.scripts.items.some(
|
||||
({ location }) =>
|
||||
location.kind === "file" && location.file_path === targetPath,
|
||||
),
|
||||
`Actual entry script was not independently resolved: ${targetPath}`,
|
||||
);
|
||||
if (expectedLocation.kind === "unresolved")
|
||||
assert.ok(
|
||||
result.unknowns.includes(
|
||||
"The discovery-reported file location cannot be verified; loaded script locations are resolved independently from Debugger.scriptParsed.",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function runMcp(name, arguments_) {
|
||||
const response = await client.callTool({ name, arguments: arguments_ });
|
||||
assert.notEqual(
|
||||
response.isError,
|
||||
true,
|
||||
JSON.stringify(response.structuredContent),
|
||||
);
|
||||
assert.ok(response.structuredContent?.result);
|
||||
return response.structuredContent.result;
|
||||
}
|
||||
|
||||
async function inspectorUrl(process_) {
|
||||
@@ -91,30 +188,41 @@ async function inspectorUrl(process_) {
|
||||
throw new Error("Node exited before reporting its Inspector URL");
|
||||
}
|
||||
|
||||
async function runCli(arguments_, environment) {
|
||||
async function runCli(arguments_) {
|
||||
const { stdout } = await execute(
|
||||
process.execPath,
|
||||
["scripts/rea.mjs", ...arguments_],
|
||||
{
|
||||
cwd: resolve(dirname(fileURLToPath(import.meta.url)), ".."),
|
||||
cwd: repository,
|
||||
env: environment,
|
||||
maxBuffer: 16 * 1_024 * 1_024,
|
||||
timeout: 30_000,
|
||||
},
|
||||
);
|
||||
return JSON.parse(stdout);
|
||||
}
|
||||
|
||||
async function terminate(process_) {
|
||||
if (process_.exitCode !== null) return;
|
||||
if (process_.exitCode !== null || process_.signalCode !== null) return;
|
||||
const exited = once(process_, "exit");
|
||||
process_.kill("SIGTERM");
|
||||
const terminated = await Promise.race([
|
||||
exited.then(() => true),
|
||||
new Promise((resolveTimeout) =>
|
||||
setTimeout(() => resolveTimeout(false), 2_000),
|
||||
),
|
||||
]);
|
||||
if (terminated || process_.exitCode !== null) return;
|
||||
process_.kill("SIGKILL");
|
||||
await exited;
|
||||
let timer;
|
||||
try {
|
||||
const terminated = await Promise.race([
|
||||
exited.then(() => true),
|
||||
new Promise((resolveTimeout) => {
|
||||
timer = setTimeout(() => resolveTimeout(false), 2_000);
|
||||
}),
|
||||
]);
|
||||
if (
|
||||
terminated ||
|
||||
process_.exitCode !== null ||
|
||||
process_.signalCode !== null
|
||||
)
|
||||
return;
|
||||
process_.kill("SIGKILL");
|
||||
await exited;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execFile } from "node:child_process";
|
||||
import { access, mkdtemp, rm } from "node:fs/promises";
|
||||
import {
|
||||
access,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { parse as parseJsonc } from "jsonc-parser";
|
||||
import { supportedClients } from "../dist/application/SupportedClients.js";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
|
||||
import { verifyCompleteToolCatalog } from "./lib/verify-package-core.mjs";
|
||||
import { verifyPackageUpdate } from "./verify-package-update.mjs";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
@@ -89,6 +99,117 @@ try {
|
||||
await client.close();
|
||||
}
|
||||
|
||||
const home = join(workspace, "home");
|
||||
const setupEnvironment = {
|
||||
...environment,
|
||||
HOME: home,
|
||||
USERPROFILE: home,
|
||||
APPDATA: join(home, "AppData", "Roaming"),
|
||||
XDG_CONFIG_HOME: join(home, ".config"),
|
||||
OPENCODE_CONFIG: join(home, ".config", "opencode", "opencode.jsonc"),
|
||||
COPILOT_HOME: join(home, ".copilot"),
|
||||
};
|
||||
await mkdir(join(home, ".config", "opencode"), { recursive: true });
|
||||
await writeFile(
|
||||
setupEnvironment.OPENCODE_CONFIG,
|
||||
'{\n // preserve user settings\n "theme": "dark",\n}\n',
|
||||
);
|
||||
const setupArgs = [
|
||||
"setup",
|
||||
"--yes",
|
||||
"--client",
|
||||
"opencode",
|
||||
"--client",
|
||||
"vscode",
|
||||
"--client",
|
||||
"copilot_cli",
|
||||
"--skill=false",
|
||||
"--json",
|
||||
];
|
||||
const setup = JSON.parse(
|
||||
(
|
||||
await exec(process.execPath, [entry, ...setupArgs], {
|
||||
env: setupEnvironment,
|
||||
windowsHide: true,
|
||||
})
|
||||
).stdout,
|
||||
);
|
||||
if (setup.status !== "ready" || setup.appliedActions.length !== 3)
|
||||
throw new Error(
|
||||
`Packaged agent-only setup failed: ${JSON.stringify(setup)}`,
|
||||
);
|
||||
const expectedCommand =
|
||||
process.platform === "win32"
|
||||
? [process.execPath, entry, "mcp"]
|
||||
: [entry, "mcp"];
|
||||
const clients = supportedClients(home, process.platform, setupEnvironment);
|
||||
for (const id of ["opencode", "vscode", "copilot_cli"]) {
|
||||
const clientLocation = clients.find(({ name }) => name === id);
|
||||
if (clientLocation === undefined)
|
||||
throw new Error(`Missing packaged client ${id}`);
|
||||
const text = await readFile(clientLocation.configPath, "utf8");
|
||||
const config = parseJsonc(text);
|
||||
const registration =
|
||||
id === "opencode"
|
||||
? config.mcp?.rea
|
||||
: id === "vscode"
|
||||
? config.servers?.rea
|
||||
: config.mcpServers?.rea;
|
||||
const actualCommand =
|
||||
id === "opencode"
|
||||
? registration?.command
|
||||
: [registration?.command, ...(registration?.args ?? [])];
|
||||
if (JSON.stringify(actualCommand) !== JSON.stringify(expectedCommand))
|
||||
throw new Error(
|
||||
`Packaged ${id} has an unusable launcher: ${JSON.stringify(actualCommand)}`,
|
||||
);
|
||||
if (id === "opencode" && !text.includes("// preserve user settings"))
|
||||
throw new Error("Packaged setup discarded OpenCode JSONC comments");
|
||||
}
|
||||
const repeated = JSON.parse(
|
||||
(
|
||||
await exec(process.execPath, [entry, ...setupArgs], {
|
||||
env: setupEnvironment,
|
||||
windowsHide: true,
|
||||
})
|
||||
).stdout,
|
||||
);
|
||||
if (
|
||||
repeated.status !== "ready" ||
|
||||
repeated.plannedActions.length !== 0 ||
|
||||
repeated.appliedActions.length !== 0
|
||||
)
|
||||
throw new Error("Packaged agent setup is not idempotent");
|
||||
const uninstalled = JSON.parse(
|
||||
(
|
||||
await exec(process.execPath, [entry, "uninstall", "--json"], {
|
||||
env: setupEnvironment,
|
||||
windowsHide: true,
|
||||
})
|
||||
).stdout,
|
||||
);
|
||||
for (const id of ["opencode", "vscode", "copilot_cli"])
|
||||
if (
|
||||
uninstalled.items?.find(({ name }) => name === id)?.status !== "removed"
|
||||
)
|
||||
throw new Error(`Packaged uninstall did not remove ${id}`);
|
||||
|
||||
if (process.platform !== "win32")
|
||||
await npm(
|
||||
["install", "--global", "--ignore-scripts", "--prefix", prefix, tarball],
|
||||
workspace,
|
||||
);
|
||||
const update = await verifyPackageUpdate({
|
||||
prefix,
|
||||
packageRoot:
|
||||
process.platform === "win32"
|
||||
? join(prefix, "node_modules", "rea-agents")
|
||||
: join(prefix, "lib", "node_modules", "rea-agents"),
|
||||
tarball,
|
||||
workspace,
|
||||
environment,
|
||||
});
|
||||
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
verifier_run: await completeVerifierRun(verifierRun),
|
||||
@@ -97,6 +218,10 @@ try {
|
||||
package: packageResult.filename,
|
||||
tools: toolCount,
|
||||
ghidra_bridge: "present",
|
||||
agent_setup: ["opencode", "vscode", "copilot_cli"],
|
||||
setup_idempotent: true,
|
||||
uninstall: true,
|
||||
update,
|
||||
})}\n`,
|
||||
);
|
||||
} finally {
|
||||
|
||||
+2
-2
@@ -8,13 +8,13 @@
|
||||
"url": "https://github.com/morluto/rea",
|
||||
"source": "github"
|
||||
},
|
||||
"version": "3.2.1",
|
||||
"version": "4.0.0",
|
||||
"packages": [
|
||||
{
|
||||
"registryType": "npm",
|
||||
"registryBaseUrl": "https://registry.npmjs.org",
|
||||
"identifier": "rea-agents",
|
||||
"version": "3.2.1",
|
||||
"version": "4.0.0",
|
||||
"runtimeHint": "npx",
|
||||
"transport": {
|
||||
"type": "stdio"
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
---
|
||||
name: reverse-engineer-anything
|
||||
description: Reverse engineer native, managed, Electron/JavaScript, packaged, and browser applications with REA. Use shipped-artifact or approved runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
|
||||
description: Reverse engineer native, managed, Electron/JavaScript, packaged, and browser applications with REA. Use shipped-artifact or requested runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
|
||||
metadata:
|
||||
version: "23"
|
||||
tool_count: 122
|
||||
catalog_digest: "a87e48bc626cb4361f494c074e71852304bfedee17fbba89976d32bb6244f230"
|
||||
version: "24"
|
||||
tool_count: 116
|
||||
catalog_digest: "5bb3f994a3c4d0c87a34ff5aab4e28141b5fa6f3a98b5ae7e95686d85d9d983e"
|
||||
---
|
||||
|
||||
# REA
|
||||
|
||||
Use REA when a claim depends on a shipped binary or package, decompilation,
|
||||
passive application runtime evidence, controlled replay, or comparison with
|
||||
passive application runtime evidence, or comparison with
|
||||
behavior not established by available source. For ordinary analysis of a
|
||||
complete source repository, use normal repository tools and do not run REA
|
||||
readiness or provider commands.
|
||||
@@ -50,8 +50,12 @@ returned result leaves a specific question unanswered.
|
||||
|
||||
Every conclusion must distinguish observations, inferences, and unknowns. Cite
|
||||
Evidence IDs, preserve limitations and incomplete coverage, and never imply
|
||||
that static analysis observed execution. Ask for approval only where a tool or
|
||||
policy requires it; approval never broadens a different authority boundary.
|
||||
that static analysis observed execution. Runtime requests execute the declared
|
||||
target and lifecycle; do not broaden the target or action beyond those fields.
|
||||
|
||||
Within the user's requested investigation, call available analysis tools
|
||||
directly. REA does not require permission grants or per-call approval flags.
|
||||
Follow the declared request scope and the host's actual access requirements.
|
||||
|
||||
## Plan broader investigations
|
||||
|
||||
@@ -71,8 +75,10 @@ For requests that span multiple features or subsystems, use a staged workflow:
|
||||
they affect the question. Use format-aware inventory and parsers; do not
|
||||
infer behavior from filenames, strings, or layout alone.
|
||||
5. Corroborate a conclusion with the evidence type it requires. Use runtime
|
||||
observation or controlled replay only when static evidence cannot answer the
|
||||
question and the required authority is available.
|
||||
observation only when static evidence cannot answer the question and the
|
||||
required host runtime and OS access are available. For JavaScript behavior,
|
||||
run probes against the actual app through the available browser, Electron,
|
||||
or process capture workflow.
|
||||
6. Decompose work into independent questions. When parallel workers are
|
||||
available and the questions do not depend on one another, assign distinct
|
||||
scopes, point workers to existing evidence, and ask them to return sources,
|
||||
@@ -96,8 +102,6 @@ as complete while required questions remain open.
|
||||
[references/runtime-observation.md](references/runtime-observation.md)
|
||||
- Evidence paging, comparisons, residual unknowns, and verification:
|
||||
[references/evidence-workflows.md](references/evidence-workflows.md)
|
||||
- Controlled JavaScript replay:
|
||||
[references/controlled-replay.md](references/controlled-replay.md)
|
||||
|
||||
## Readiness and setup
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# Controlled JavaScript replay
|
||||
|
||||
Use `run_controlled_replay` only for operator-selected extracted modules when
|
||||
the separate replay policy is enabled. Call `mode: plan` first. Review the exact
|
||||
module, stub, runtime, sandbox, case, limits, and policy commitments. Execute
|
||||
only with `approved: true` and that exact plan digest.
|
||||
|
||||
Treat return values, exceptions, denials, limits, and crashes as observations of
|
||||
the isolated experiment with controlled-replay authority, not facts about the
|
||||
real application. Never substitute browser/Electron permission, process
|
||||
capture, or an in-process `vm` run. Reproducer export requires separate literal
|
||||
approval and evidence-write authority after sandbox cleanup.
|
||||
@@ -7,8 +7,9 @@ is incomplete or paginated, continue only when the remaining data matters to
|
||||
the task; do not fetch a bundle or resource merely to read a result already
|
||||
returned inline.
|
||||
|
||||
Use `record_unknown` only with explicit approval and name the authority or
|
||||
environment still required. Supply supporting and contradicting evidence IDs.
|
||||
Use `record_unknown` to track unresolved questions in the user's investigation
|
||||
and name the authority or environment still required. Supply supporting and
|
||||
contradicting evidence IDs. It updates the session registry directly.
|
||||
Use `update_unknown` with the current revision; reread after a stale revision
|
||||
instead of retrying blindly. Only qualifying observed evidence can verify a
|
||||
resolution.
|
||||
|
||||
@@ -31,5 +31,6 @@ Include the complete Evidence records from both analysis calls. Accept variant
|
||||
pairing only through the tool's unique exact literal discriminant. Cite the
|
||||
comparison Evidence and report JSON Pointer changes; dynamic values, ambiguous
|
||||
variants, and incomplete parent-property coverage stay unknown. This is static
|
||||
inference, not runtime behavior. Use `run_controlled_replay` separately only
|
||||
when the user needs approved runtime semantics.
|
||||
inference, not runtime behavior. When runtime semantics are needed, run
|
||||
behavioral probes against the relevant application versions and capture them
|
||||
through the available browser, Electron, or process workflows.
|
||||
|
||||
@@ -32,9 +32,11 @@ IDs when using them.
|
||||
Use `extract_artifact` when materialized files are needed. It takes no arguments
|
||||
and materializes all regular files into a fresh temporary directory chosen by
|
||||
REA. Symlinks and encrypted entries are inventory facts, not extractable files.
|
||||
The operation still requires the filesystem-write permission grant; callers do
|
||||
not choose the destination.
|
||||
The result reports the materialized directory; callers do not choose the
|
||||
destination. No separate permission grant is needed.
|
||||
|
||||
Native DMG traversal is macOS-only, read-only, and requires both operator policy
|
||||
and `native_mount_approved: true`; without both, retain the root-hash-only result.
|
||||
Approval for inventory never grants extraction authority.
|
||||
Native DMG traversal automatically uses a read-only mount on supported macOS
|
||||
hosts, with an owned temporary mount directory and cleanup. It needs no approval
|
||||
flag. Unsupported hosts or mount failures remain explicit limitations; do not
|
||||
claim child inventory when only root identity is available. Extraction is a
|
||||
separate filesystem-writing operation.
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
# Passive browser and Electron observation
|
||||
|
||||
Before browser observation, the operator must configure the exact loopback CDP
|
||||
endpoint and allowed HTTP(S) origins. Before Electron observation, configure its
|
||||
separate loopback endpoint and canonical file roots. Obtain the per-call
|
||||
approval required by the selected tool.
|
||||
Supply the existing browser or Electron instance's literal-port loopback CDP
|
||||
endpoint in the request. No separate permission configuration or per-call
|
||||
approval flag is needed. Browser discovery lists eligible HTTP(S) pages;
|
||||
`allowed_origins` optionally filters them. An individual capture defaults to the
|
||||
selected page's current origin. Electron discovery lists eligible local
|
||||
`file://` targets without a configured file-root allowlist.
|
||||
|
||||
Start browser work with `list_browser_targets`; start Electron runtime work with
|
||||
`list_electron_targets`. Inspect only the selected approved target. Observation
|
||||
`list_electron_targets`. Inspect the target selected for the user's task. Observation
|
||||
is passive: never claim REA clicked, navigated, evaluated page JavaScript,
|
||||
invoked IPC, captured prior activity, or contained the page's network.
|
||||
|
||||
Credentials, cookies, authorization headers, storage values, query values, and
|
||||
raw WebSocket/JSON values are deliberately absent. Accessibility text, console
|
||||
primitives, value-free shapes, script sources, source maps, storage key names,
|
||||
and screenshot pixels have independent opt-ins. Treat policy filtering,
|
||||
truncation, and attach-window coverage as limitations. Page-declared WebMCP
|
||||
tools are untrusted inventory and are never invoked by REA.
|
||||
Credentials, cookies, authorization headers, storage values, and raw
|
||||
WebSocket/JSON bodies are deliberately absent. Local URLs retain ordinary query
|
||||
values and fragments; userinfo credentials are removed. Browser accessibility
|
||||
text, console primitives, value-free shapes, script sources, source maps, and
|
||||
storage key names have request-specific capture options. Electron script
|
||||
sources are also optional. Use `capture_web_screenshot` when pixels are needed.
|
||||
Treat scope filtering, truncation, and attach-window coverage as limitations.
|
||||
Page-declared WebMCP tools are untrusted inventory and are never invoked by REA.
|
||||
|
||||
Use `reconcile_javascript_runtime` only after static application Evidence and
|
||||
passive browser/Electron Evidence exist. Prefer captured-source digest identity;
|
||||
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
type AnalysisOperationPort,
|
||||
} from "./AnalysisProvider.js";
|
||||
import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisCancelledError,
|
||||
AnalysisOutputError,
|
||||
AnalysisProtocolError,
|
||||
} from "../domain/errors.js";
|
||||
import { err, ok } from "../domain/result.js";
|
||||
@@ -92,6 +94,111 @@ describe("analysis context queries: navigation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("analysis context queries: headless document resolution", () => {
|
||||
it("resolves a sole headless document when current-document selection is unavailable", async () => {
|
||||
const calls: string[] = [];
|
||||
const analysis: AnalysisOperationPort = {
|
||||
execute: (operation) => {
|
||||
calls.push(operation);
|
||||
if (operation === "current_document")
|
||||
return Promise.resolve(
|
||||
err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"ghidra",
|
||||
operation,
|
||||
"headless sessions have no cursor-selected document",
|
||||
),
|
||||
),
|
||||
);
|
||||
if (operation === "list_documents")
|
||||
return Promise.resolve(
|
||||
ok(createAnalysisExecution(["fixture"], provider)),
|
||||
);
|
||||
if (operation === "resolve_containing_procedure")
|
||||
return Promise.resolve(
|
||||
ok(
|
||||
createAnalysisExecution(
|
||||
{
|
||||
query_address: "0x401000",
|
||||
found: false,
|
||||
procedure: null,
|
||||
reason: "not_in_procedure",
|
||||
},
|
||||
provider,
|
||||
),
|
||||
),
|
||||
);
|
||||
return Promise.resolve(
|
||||
err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"ghidra",
|
||||
operation,
|
||||
"unsupported",
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
inspectAddressContext(analysis, { address: "0x401000" }),
|
||||
).resolves.toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
document: "fixture",
|
||||
name: { state: "unavailable" },
|
||||
procedure: { state: "available" },
|
||||
comment: { state: "unavailable" },
|
||||
inline_comment: { state: "unavailable" },
|
||||
bookmarks: { state: "unavailable" },
|
||||
},
|
||||
});
|
||||
expect(calls.slice(0, 2)).toEqual(["current_document", "list_documents"]);
|
||||
});
|
||||
|
||||
it("does not use list-documents fallback for non-capability document errors", async () => {
|
||||
const failure = new AnalysisProtocolError(
|
||||
"current-document response malformed",
|
||||
);
|
||||
const calls: string[] = [];
|
||||
const analysis: AnalysisOperationPort = {
|
||||
execute: (operation) => {
|
||||
calls.push(operation);
|
||||
return Promise.resolve(err(failure));
|
||||
},
|
||||
};
|
||||
|
||||
await expect(
|
||||
inspectAddressContext(analysis, { address: "0x401000" }),
|
||||
).resolves.toEqual(err(failure));
|
||||
expect(calls).toEqual(["current_document"]);
|
||||
});
|
||||
|
||||
it("requires exactly one listed document for a headless fallback", async () => {
|
||||
const analysis: AnalysisOperationPort = {
|
||||
execute: (operation) =>
|
||||
Promise.resolve(
|
||||
operation === "current_document"
|
||||
? err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"ghidra",
|
||||
operation,
|
||||
"no selected document",
|
||||
),
|
||||
)
|
||||
: ok(createAnalysisExecution(["one", "two"], provider)),
|
||||
),
|
||||
};
|
||||
|
||||
await expect(
|
||||
inspectAddressContext(analysis, { address: "0x401000" }),
|
||||
).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: expect.any(AnalysisOutputError),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("analysis context queries: address facets", () => {
|
||||
it("keeps unsupported address facets local and filters bookmarks", async () => {
|
||||
const bookmarkInputs: Readonly<Record<string, unknown>>[] = [];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user