Compare commits
207
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a64851f259 | ||
|
|
c267e99227 | ||
|
|
4bb4c6fe6d | ||
|
|
411498d45f | ||
|
|
c3fc6c3aed | ||
|
|
1f099a6fc0 | ||
|
|
701d90aba4 | ||
|
|
a8701eaf48 | ||
|
|
ad59531a96 | ||
|
|
69c3b32a21 | ||
|
|
5f749c2096 | ||
|
|
76b214e45f | ||
|
|
614e0c2e74 | ||
|
|
73e98b2516 | ||
|
|
99f257bbf0 | ||
|
|
b9e29c52d4 | ||
|
|
1c3766a8bc | ||
|
|
098d76900e | ||
|
|
2ddd9456cd | ||
|
|
43405f9ffe | ||
|
|
6020a0b368 | ||
|
|
fe34ec2ef3 | ||
|
|
ad813e473b | ||
|
|
1e341973cf | ||
|
|
69833fb0d0 | ||
|
|
184ed970b9 | ||
|
|
8c9c5550d0 | ||
|
|
fb57c8643f | ||
|
|
48de11ce2e | ||
|
|
b80bd2db96 | ||
|
|
d601b8016e | ||
|
|
39eb0587c9 | ||
|
|
db98a51dd1 | ||
|
|
59fbaa2d33 | ||
|
|
7b51121f7c | ||
|
|
33bf585185 | ||
|
|
f61673fa99 | ||
|
|
7e96f25f8b | ||
|
|
502528c58d | ||
|
|
bb8ff2cfba | ||
|
|
b8851cb977 | ||
|
|
2060a6d7f3 | ||
|
|
09058ebc4a | ||
|
|
1e83d270c2 | ||
|
|
0c6852b392 | ||
|
|
a8b8288396 | ||
|
|
5486860673 | ||
|
|
f085589957 | ||
|
|
fd46cff017 | ||
|
|
d6d1cd2485 | ||
|
|
af7a439633 | ||
|
|
81b70bc358 | ||
|
|
5eb0049a9f | ||
|
|
b7b4333703 | ||
|
|
0696bde266 | ||
|
|
88640f60fe | ||
|
|
7bf63ed7f8 | ||
|
|
636b245f1b | ||
|
|
b71a591a60 | ||
|
|
da606ff777 | ||
|
|
2f02eff988 | ||
|
|
5510b27d7f | ||
|
|
475a0f84da | ||
|
|
63bf91405c | ||
|
|
2a7746998d | ||
|
|
abc4f47683 | ||
|
|
c8544b1574 | ||
|
|
4578647c59 | ||
|
|
cc39fc53b8 | ||
|
|
13c3b68730 | ||
|
|
18a1e8cd0e | ||
|
|
933fcb33c1 | ||
|
|
f6fe575094 | ||
|
|
8342e3a78f | ||
|
|
bfaa5a6c2c | ||
|
|
b189c3ceb1 | ||
|
|
bcc8a773e2 | ||
|
|
6cc817e8bd | ||
|
|
9426c3f1bb | ||
|
|
5691606aa9 | ||
|
|
45e3edc17f | ||
|
|
04aef6df33 | ||
|
|
552634d634 | ||
|
|
0e779533a9 | ||
|
|
c5c75c38c3 | ||
|
|
569d2bc6b9 | ||
|
|
889fb374df | ||
|
|
24a3aa08d1 | ||
|
|
00d8e44cbf | ||
|
|
3fb9ffaa4d | ||
|
|
db0afce5f3 | ||
|
|
5612496f6d | ||
|
|
fe5291e916 | ||
|
|
d4a598e936 | ||
|
|
ddaa18301a | ||
|
|
d6832c3632 | ||
|
|
9b6950373c | ||
|
|
6020df53d9 | ||
|
|
a6fdf426bd | ||
|
|
db177c91b8 | ||
|
|
d186ca7e41 | ||
|
|
aa5750c263 | ||
|
|
8c5e34cd37 | ||
|
|
0270dd5739 | ||
|
|
0a14bbbf05 | ||
|
|
f6f5c16097 | ||
|
|
f4c99ab8a5 | ||
|
|
954badbaaa | ||
|
|
22958558ea | ||
|
|
fef38e4d22 | ||
|
|
136d4aab18 | ||
|
|
68cb02fe8b | ||
|
|
8c5088c8cd | ||
|
|
3d328193d6 | ||
|
|
f0da05f67f | ||
|
|
01174c4a81 | ||
|
|
6489d58851 | ||
|
|
59c91c88fe | ||
|
|
c89b3a00dc | ||
|
|
170d0bf9e2 | ||
|
|
6935dc7fe1 | ||
|
|
04a516b1e3 | ||
|
|
4b38d18995 | ||
|
|
c3f169823f | ||
|
|
063de5014f | ||
|
|
e6905c18e0 | ||
|
|
9405bd660d | ||
|
|
27a9ceade7 | ||
|
|
c399670653 | ||
|
|
863674b2e9 | ||
|
|
c2d8146f94 | ||
|
|
c94f06d98f | ||
|
|
5b769d3a78 | ||
|
|
6987cb066a | ||
|
|
7e9c91199e | ||
|
|
9db1fd0bd7 | ||
|
|
796b65e1cd | ||
|
|
beca9977eb | ||
|
|
6051cb2c0c | ||
|
|
864160d885 | ||
|
|
db675f1d52 | ||
|
|
911152a04d | ||
|
|
0fec65d06b | ||
|
|
cf415619ca | ||
|
|
cfb49eb8a8 | ||
|
|
8998438c23 | ||
|
|
c3acc34475 | ||
|
|
761881cec3 | ||
|
|
b79d379c25 | ||
|
|
6fbdbd6820 | ||
|
|
53568568bf | ||
|
|
f36c10f9e8 | ||
|
|
d39a450bdf | ||
|
|
98b07d07dd | ||
|
|
f198993f41 | ||
|
|
54c0ed42aa | ||
|
|
0a0244db42 | ||
|
|
9f5dc52be8 | ||
|
|
42c276af1e | ||
|
|
291bc6fb70 | ||
|
|
facaae865a | ||
|
|
d0ff57a45c | ||
|
|
0b04d384cc | ||
|
|
e6abaefbbc | ||
|
|
f5539cfc2f | ||
|
|
2a620c06cd | ||
|
|
e750c262fb | ||
|
|
c717204795 | ||
|
|
e1afbc1914 | ||
|
|
a503376b2f | ||
|
|
4fb2f3e6ed | ||
|
|
d8e314cb24 | ||
|
|
19c52f5e3f | ||
|
|
b8312fa8d1 | ||
|
|
fd975c75fa | ||
|
|
e9c48a61e4 | ||
|
|
b4927a0a4f | ||
|
|
5cc841d5c1 | ||
|
|
11b9f0346c | ||
|
|
cf77d073fc | ||
|
|
de5bfec79f | ||
|
|
37025cc09f | ||
|
|
0b32577a90 | ||
|
|
eacf8461da | ||
|
|
b34bdcdb28 | ||
|
|
de43da71ae | ||
|
|
6a1e558db3 | ||
|
|
8e74c9c62f | ||
|
|
44d5b4b25e | ||
|
|
637bcb5e41 | ||
|
|
33d5ae0b52 | ||
|
|
24a30fd91c | ||
|
|
b620e2a2bd | ||
|
|
58a91f03d7 | ||
|
|
5160d9b717 | ||
|
|
089bbf58f3 | ||
|
|
72201668ed | ||
|
|
84094af54a | ||
|
|
16624f1324 | ||
|
|
9664b2b101 | ||
|
|
2609352eef | ||
|
|
90f532feb1 | ||
|
|
e9ebfb94ef | ||
|
|
13935077ba | ||
|
|
441813c070 | ||
|
|
c12bad7f13 | ||
|
|
70e5c24718 |
@@ -102,7 +102,7 @@ jobs:
|
||||
with:
|
||||
name: rea-dist
|
||||
path: dist
|
||||
- name: Restore generated outputs from downloaded build
|
||||
- name: Generate test artifacts from the downloaded runtime
|
||||
run: |
|
||||
node scripts/generate-mcp-tool-catalog.mjs
|
||||
node scripts/generate-skill-metadata.mjs
|
||||
@@ -334,7 +334,7 @@ jobs:
|
||||
- name: Process filesystem snapshot descriptor regressions
|
||||
run: npm run test:focused -- src/process/capture/FilesystemSnapshot.test.ts
|
||||
- name: Native Darwin ownership and process cleanup regressions
|
||||
run: npm run test:focused -- tests/boundary/process/darwinProcessRunTokenReader.test.ts tests/boundary/process/processCaptureLifecycle.test.ts tests/boundary/process/processCli.test.ts tests/boundary/mcp/sessionMcp.test.ts
|
||||
run: npm run test:focused -- tests/boundary/process/darwinProcessRunTokenReader.test.ts tests/boundary/process/directAnalysisSignals.test.ts tests/boundary/process/processCaptureLifecycle.test.ts tests/boundary/process/processCli.test.ts tests/boundary/mcp/sessionMcp.test.ts
|
||||
|
||||
reconstruction-readiness:
|
||||
name: Reconstruction readiness
|
||||
@@ -450,7 +450,6 @@ jobs:
|
||||
tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts
|
||||
tests/boundary/filesystem/setupUnavailableProvider.test.ts
|
||||
tests/boundary/filesystem/windowsNpxRegistration.test.ts
|
||||
src/application/ClientRegistrationIdentity.test.ts
|
||||
tests/boundary/filesystem/webScriptManifestPaths.test.ts
|
||||
tests/boundary/process/ProcessEvidence.host.test.ts
|
||||
tests/acceptance/webModuleCli.test.ts
|
||||
|
||||
@@ -5,8 +5,7 @@ on:
|
||||
branches: [main, "release/*"]
|
||||
paths:
|
||||
- "website/**"
|
||||
- "scripts/verify-website.py"
|
||||
- "scripts/prepare-website.py"
|
||||
- "scripts/*website*.py"
|
||||
- ".github/workflows/**"
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -21,11 +20,24 @@ jobs:
|
||||
check:
|
||||
name: Website checks
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Prepare the example download
|
||||
- name: Set up Python for website assets
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
cache-dependency-path: website/requirements.txt
|
||||
- name: Install sharing-image dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends -y libcairo2 fonts-dejavu-core
|
||||
python3 -m pip install -r website/requirements.txt
|
||||
- name: Prepare website assets
|
||||
run: python3 scripts/prepare-website.py
|
||||
- name: Check website links and publishing configuration
|
||||
- name: Check website links, metadata and publishing configuration
|
||||
run: python3 scripts/verify-website.py
|
||||
- name: Check sitemap discovery and metadata regressions
|
||||
run: python3 scripts/test-website.py
|
||||
|
||||
@@ -2,6 +2,15 @@ name: Publish REA website
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target:
|
||||
description: Publish both hosts, or Pages while configuring Cloudflare
|
||||
type: choice
|
||||
required: true
|
||||
default: both
|
||||
options:
|
||||
- both
|
||||
- pages
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -14,17 +23,58 @@ jobs:
|
||||
prepare:
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
pages: read
|
||||
steps:
|
||||
- name: Validate the publication target
|
||||
env:
|
||||
PUBLISH_TARGET: ${{ inputs.target }}
|
||||
run: |
|
||||
case "$PUBLISH_TARGET" in
|
||||
both|pages) ;;
|
||||
*) echo "::error::Unknown publication target: $PUBLISH_TARGET"; exit 1 ;;
|
||||
esac
|
||||
- name: Require Cloudflare publishing credentials
|
||||
if: inputs.target == 'both'
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
run: |
|
||||
if [[ -z "$CLOUDFLARE_API_TOKEN" || -z "$CLOUDFLARE_ACCOUNT_ID" ]]; then
|
||||
echo "::error::Set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID in repository Actions secrets before publishing."
|
||||
exit 1
|
||||
fi
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Prepare the example download
|
||||
- name: Set up Python for website assets
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
cache-dependency-path: website/requirements.txt
|
||||
- name: Install sharing-image dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends -y libcairo2 fonts-dejavu-core
|
||||
python3 -m pip install -r website/requirements.txt
|
||||
- name: Prepare website assets
|
||||
run: python3 scripts/prepare-website.py
|
||||
- name: Check website links and publishing configuration
|
||||
- name: Check website links, metadata and publishing configuration
|
||||
run: python3 scripts/verify-website.py
|
||||
- name: Record the published commit
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
Path("website/public/deployment.json").write_text(
|
||||
json.dumps({"commit": os.environ["GITHUB_SHA"]}) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
PY
|
||||
- name: Configure Pages
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Upload the website
|
||||
@@ -35,14 +85,96 @@ jobs:
|
||||
deploy:
|
||||
needs: prepare
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
pages: write
|
||||
id-token: write
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Check out publishing configuration
|
||||
if: inputs.target == 'both'
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Node.js for Wrangler
|
||||
if: inputs.target == 'both'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
- name: Download the verified website
|
||||
if: inputs.target == 'both'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: github-pages
|
||||
path: ${{ runner.temp }}/website-archive
|
||||
- name: Extract the shared publishing artifact
|
||||
if: inputs.target == 'both'
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/rea-website-public"
|
||||
tar -xf "$RUNNER_TEMP/website-archive/artifact.tar" -C "$RUNNER_TEMP/rea-website-public"
|
||||
- name: Publish the website to Cloudflare
|
||||
if: inputs.target == 'both'
|
||||
uses: cloudflare/wrangler-action@v4
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
wranglerVersion: "4.149.0"
|
||||
packageManager: npm
|
||||
command: deploy --cwd website --env production --assets "${{ runner.temp }}/rea-website-public"
|
||||
- name: Publish to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
- name: Verify the selected published revisions
|
||||
env:
|
||||
PAGES_URL: ${{ steps.deployment.outputs.page_url }}
|
||||
PUBLISH_TARGET: ${{ inputs.target }}
|
||||
run: |
|
||||
python3 -u - <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from urllib.error import URLError
|
||||
from urllib.parse import urljoin
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
expected = os.environ["GITHUB_SHA"]
|
||||
pages_url = os.environ["PAGES_URL"]
|
||||
if not pages_url:
|
||||
sys.exit("GitHub Pages returned no published URL.")
|
||||
target = os.environ["PUBLISH_TARGET"]
|
||||
if target not in ("both", "pages"):
|
||||
sys.exit("Unknown publication target: " + target)
|
||||
hosts = {"GitHub Pages": pages_url}
|
||||
if target == "both":
|
||||
hosts["Cloudflare"] = "https://rea.tools/"
|
||||
pending = dict(hosts)
|
||||
for attempt in range(24):
|
||||
for name, base in list(pending.items()):
|
||||
url = urljoin(base, "deployment.json") + "?commit=" + expected
|
||||
try:
|
||||
request = Request(url, headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"User-Agent": "REA-Website-Verifier/1.0 (+https://github.com/morluto/rea)",
|
||||
})
|
||||
with urlopen(request, timeout=10) as response:
|
||||
actual = json.load(response).get("commit")
|
||||
if actual == expected:
|
||||
print(f"{name} serves {expected}: {base}")
|
||||
del pending[name]
|
||||
else:
|
||||
print(f"Waiting for {name}: observed revision {actual}")
|
||||
except (URLError, TimeoutError, ValueError) as error:
|
||||
print(f"Waiting for {name}: {error}")
|
||||
if not pending:
|
||||
break
|
||||
if attempt < 23:
|
||||
time.sleep(5)
|
||||
if pending:
|
||||
sys.exit("Could not verify published revision on: " + ", ".join(pending))
|
||||
with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as summary:
|
||||
links = " and ".join(f"[{name}]({url})" for name, url in hosts.items())
|
||||
summary.write(f"Published commit `{expected}` to {links}.\n")
|
||||
PY
|
||||
|
||||
+5
-4
@@ -49,6 +49,10 @@ Thumbs.db
|
||||
|
||||
# Generated website download
|
||||
/website/public/examples/notes-example.zip
|
||||
/website/public/deployment.json
|
||||
/website/public/sitemap.xml
|
||||
/website/public/assets/social-card.png
|
||||
/website/.venv/
|
||||
|
||||
# Local Cloudflare development state
|
||||
/website/.wrangler/
|
||||
@@ -57,11 +61,8 @@ Thumbs.db
|
||||
!/website/.dev.vars.example
|
||||
!/website/.env*.example
|
||||
|
||||
# Build-generated MCP tool catalog (regenerated by `npm run build`; never commit)
|
||||
src/generatedMcpToolCatalog.ts
|
||||
|
||||
# Derived catalogs, portable conformance projections, and packaged skill bundles
|
||||
# (regenerated from this checkout by `npm run build:cached`; never commit)
|
||||
# (regenerated by `npm run docs:generate`; never commit)
|
||||
/skills/
|
||||
/docs/public/product-catalog.json
|
||||
/docs/verification/managed-conformance-manifest.json
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "6.1.0"
|
||||
".": "6.2.0"
|
||||
}
|
||||
|
||||
@@ -24,15 +24,15 @@ REA is a layered ESM TypeScript application. Dependencies flow inward from pure
|
||||
- `src/application/` composes shared CLI/MCP workflows, including Evidence provenance, unknowns, and eligible snapshot bindings; `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. Reuse its supervision and identity primitives before adding provider-local lifecycle code; a PID and executable pathname alone do not establish ownership after exit or reuse. `bridge/` contains provider-side adapters.
|
||||
- `tests/` contains unit, composition, boundary, acceptance, and conformance tests. `scripts/verify-*` and capability directories under `scripts/verify/` contain real-toolchain checks.
|
||||
- `docs/public/product-catalog.json`, `docs/verification/managed-conformance-*.json`, and `skills/` are ignored build outputs. Update source contracts and authored instructions in `skill-src/`, then run `npm run build:cached`; never commit derived catalog digests or portable conformance projections.
|
||||
- `src/generatedMcpToolCatalog.ts` is build-generated and gitignored. Never commit it; resolve any trace of it in merges by deleting it and running `npm run build:cached`.
|
||||
- `docs/public/product-catalog.json`, `docs/verification/managed-conformance-*.json`, and `skills/` are ignored build outputs. Update source contracts and authored instructions in `skill-src/`, then run `npm run docs:generate`; never commit derived catalog digests or portable conformance projections.
|
||||
- `.cache/mcp-tool-catalog.json` is generated test metadata. Generate it with `npm run mcp-catalog:generate`; source checking and runtime builds do not consume it.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
- `npm ci`: install the locked dependencies.
|
||||
- `npm run build:cached`: build the CLI and MCP server.
|
||||
- `npm run build:cached`: compile the CLI/MCP runtime and bundle its authored skill. Test catalogs and documentation/conformance artifacts have separate tasks.
|
||||
- `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 test:focused -- PATH...`: run exact test files, preparing runtime and generated artifacts required by the selected 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`: 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.
|
||||
@@ -50,6 +50,8 @@ Configuration is parsed and validated by `src/config.ts` and `src/config/`. Keep
|
||||
|
||||
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.
|
||||
|
||||
Assess brittleness during implementation, debugging, review, and maintenance. Simplify duplicated rules, scattered defaults, caller-specific exceptions, and unclear invariants in touched code. Prefer standard format-aware parsers and existing shared helpers; normalize once at the owning boundary while preserving source evidence. Keep refactors focused on the task, and introduce abstractions only when they remove observed complexity. Verify preserved behavior and corrected edge cases through affected callers.
|
||||
|
||||
## 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.
|
||||
@@ -92,7 +94,9 @@ See [docs/tool-design.md](docs/tool-design.md) for the design checklist. When us
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
Prioritize real end-to-end workflows, boundary integration, and golden producer data. Keep a focused module test when it covers a distinct failure or semantic case absent from those workflows; test paths do not establish depth. See [docs/testing.md](docs/testing.md) for pruning and classification rules.
|
||||
Prefer full end-to-end workflows through the public CLI or MCP with real providers and no mocked dependencies, then integration across production boundaries, then goldens captured from real producers. Keep a focused module test only for a distinct failure or semantic case that stronger workflows cannot reliably reproduce. Delete redundant getter, serialization, enum, count, and snapshot assertions when a stronger consumer workflow already proves the claim. Test paths, compiled imports, and suites named E2E do not establish end-to-end coverage. See [docs/testing.md](docs/testing.md) for pruning and classification rules.
|
||||
|
||||
Treat advertised examples as executable contracts: derive them from representative producer data, exercise them through the advertised CLI or MCP workflow, and assert the behavior they claim to demonstrate. Share production projections where practical and declare omitted coverage explicitly.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
+244
@@ -1,5 +1,248 @@
|
||||
# Changelog
|
||||
|
||||
## [6.2.0](https://github.com/morluto/rea/compare/rea-agents-6.1.0...rea-agents-6.2.0) (2026-10-09)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
Read the [6.1 to 6.2 migration guide](docs/migration-6.2.md) before upgrading. REA's `always-bump-minor` policy includes breaking changes; 6.2 is not a backward-compatibility promise. Preserve original saved files and regenerate missing observations rather than fabricating identities or facts.
|
||||
|
||||
* **MCP / Evidence / snapshots / artifacts:** Success results are direct canonical Evidence; read `normalized_result` rather than the old `result`/`evidence` wrapper. Evidence requires nullable `analysis_profile`, session provider aliases are removed, snapshots require complete profile-bound query records, and artifact roles/permissions belong to occurrences. Recreate incompatible saved data; adding fields without recomputing their semantic identity is invalid. ([63bf914](https://github.com/morluto/rea/commit/63bf91405cf97b114a84f337fb1272d98a267999))
|
||||
* **Process captures:** Legacy v3 conversion is removed. Explicit executable identity and `event_journal` are required; retain old captures and recapture when current comparisons are needed instead of inventing launch identity or event order. ([2a77469](https://github.com/morluto/rea/commit/2a7746998d47446c4db6d4f524e0f183d6803f3f))
|
||||
* **Cleanup and evaluation:** A one-shot analysis reports cleanup failure while preserving the completed observation or primary diagnostic. Handle `cleanup_incomplete` and retained observations/resources. Configured agent-evaluation fixtures require Evidence-backed correctness, not routing or answer wording alone. ([#1211](https://github.com/morluto/rea/pull/1211), [27a9cea](https://github.com/morluto/rea/commit/27a9ceade7232c1c9a24dc7d454ae060948183a1))
|
||||
* **JavaScript:** Return-field projections require explicit presence; export-shape changes carry both sides' presence and property inventory coverage. Semantic object operations retain complete `property_path` segments. Regenerate old analysis and distinguish unknown values from unknown presence. ([475a0f8](https://github.com/morluto/rea/commit/475a0f84dac63d710e032bc7f731c1a3bda624ec))
|
||||
* **Function comparisons:** Remove the unreachable `truncated` dimension state and `summary.truncated` counter; update enum and display consumers while retaining explicit `unknown` facts. ([5510b27](https://github.com/morluto/rea/commit/5510b27d7f863a3df26212fba4a59f8df0da3712))
|
||||
* **Historical sources:** Unreadable/unknown symlink targets require `target: null`; external targets retain the observed absolute target. Reimport graphs containing display placeholders such as `<outside-root>` and handle nullable targets. ([b7b4333](https://github.com/morluto/rea/commit/b7b433370310c3ffe33d5027d8393151d5e862da))
|
||||
|
||||
### Features
|
||||
|
||||
* **javascript:** report observed export property presence ([#1160](https://github.com/morluto/rea/issues/1160)) ([45e3edc](https://github.com/morluto/rea/commit/45e3edc17f48954c89d8db86f7313c479bd39344))
|
||||
* **mcp:** select views of retained analysis results ([#1180](https://github.com/morluto/rea/issues/1180)) ([db675f1](https://github.com/morluto/rea/commit/db675f1d52acda62b6f57b5a8773532f49b259e7))
|
||||
* **website:** add an Aegis APK code-generation showcase ([0270dd5](https://github.com/morluto/rea/commit/0270dd5739480acced62e045b5890ec0ebde2631))
|
||||
* **website:** add an Android showcase and clearer project prompts ([6cc817e](https://github.com/morluto/rea/commit/6cc817e8bd377b064b3568ef1eacb27eaefcb4c1))
|
||||
* **website:** add beginner examples and synchronized publishing ([0fec65d](https://github.com/morluto/rea/commit/0fec65d06b2f5431e704c472e53015d4b3cc967c))
|
||||
* **website:** add beginner examples and synchronized publishing ([#1203](https://github.com/morluto/rea/issues/1203)) ([0fec65d](https://github.com/morluto/rea/commit/0fec65d06b2f5431e704c472e53015d4b3cc967c))
|
||||
* **website:** add Discord link beside GitHub badge ([cc39fc5](https://github.com/morluto/rea/commit/cc39fc53b829a349ae199823472990a53014c507))
|
||||
* **website:** add homepage navigation and faster reading paths ([863674b](https://github.com/morluto/rea/commit/863674b2e92dd7c699768db82d521ccc88b7ebe4))
|
||||
* **website:** add homepage navigation and faster reading paths ([c94f06d](https://github.com/morluto/rea/commit/c94f06d98f895eb8b6c5ffa5e999ef1d4bb5eea2))
|
||||
* **website:** add homepage navigation and faster reading paths ([#1213](https://github.com/morluto/rea/issues/1213)) ([863674b](https://github.com/morluto/rea/commit/863674b2e92dd7c699768db82d521ccc88b7ebe4))
|
||||
* **website:** add search metadata and automatic sitemap ([99f257b](https://github.com/morluto/rea/commit/99f257bbf03c27327e5e0a232fefd3c792acd5bc))
|
||||
* **website:** add search metadata and automatic sitemap ([b8851cb](https://github.com/morluto/rea/commit/b8851cb977cfdd608798d32bf5877f645b960831)), closes [#1283](https://github.com/morluto/rea/issues/1283)
|
||||
* **website:** clarify reverse engineering and showcase goals ([e750c26](https://github.com/morluto/rea/commit/e750c262fbf37100369ab25bf8659daa1a2c4cc7))
|
||||
* **website:** explain REA through a first investigation ([5160d9b](https://github.com/morluto/rea/commit/5160d9b717d0207a357c754bdf72e215e59d19d1))
|
||||
* **website:** explain REA with Calculator and a playable dinosaur lab ([11b9f03](https://github.com/morluto/rea/commit/11b9f0346cf5413045b41ff552a024e11ba46ccc))
|
||||
* **website:** lead with the playable dinosaur example ([19c52f5](https://github.com/morluto/rea/commit/19c52f5e3f42fb754fc154bbd16552c5111df0c5))
|
||||
* **website:** show manual analysis beside a one-prompt workflow ([291bc6f](https://github.com/morluto/rea/commit/291bc6fb70f47d28d23440f03a8a7fa49eb9a391))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **dev:** accept formatter-ignored staged documentation without failing the commit hook; malformed JSON remains rejected. ([bd2db2f](https://github.com/morluto/rea/commit/bd2db2f0fa495fb08dea443cd16d7683d4b86e6a))
|
||||
|
||||
* **release:** publish reviewed release-PR merges from their exact reviewed source. ([c12bad7](https://github.com/morluto/rea/commit/c12bad7f139431fc0db7a8b7c9db1eab95f96dc2))
|
||||
|
||||
* **android:** support owned JADX stdio on Windows ([#1276](https://github.com/morluto/rea/issues/1276)) ([636b245](https://github.com/morluto/rea/commit/636b245f1badf2a7ecfa63c6e2378ac2f7673cc3)), closes [#1262](https://github.com/morluto/rea/issues/1262)
|
||||
* **android:** validate engine archive prerequisites ([#1145](https://github.com/morluto/rea/issues/1145)) ([e9ebfb9](https://github.com/morluto/rea/commit/e9ebfb94eff0699b1ea84bba000c4467fa1cb998))
|
||||
* **apple:** report unusable dylib-resolution roots as invalid input ([6a1e558](https://github.com/morluto/rea/commit/6a1e558db3ec2b69561e91ea817f1c2065dfc676))
|
||||
* **apple:** report unusable dylib-resolution roots as invalid input ([24a30fd](https://github.com/morluto/rea/commit/24a30fd91c144b7f2533e5f5de8c91268773519c))
|
||||
* **artifacts:** keep cancellation and the caller path in extraction refusals ([37025cc](https://github.com/morluto/rea/commit/37025cc09f12d49f46e61eedc3564c710089146b))
|
||||
* **artifacts:** keep cancellation and the caller path in extraction refusals ([eacf846](https://github.com/morluto/rea/commit/eacf8461dad6bab11a3d5438e6db001a10f33a9b))
|
||||
* **artifacts:** match lipo arm64e variant names to their FAT slices ([de5bfec](https://github.com/morluto/rea/commit/de5bfec79fe862816350b2deeb42a12c0734d855))
|
||||
* **artifacts:** match lipo arm64e variant names to their FAT slices ([0b32577](https://github.com/morluto/rea/commit/0b32577a90aef97e36adf2a7af4f3eccd5d82357)), closes [#1168](https://github.com/morluto/rea/issues/1168)
|
||||
* **artifacts:** name only host-available workflows for non-archive plists ([f4c99ab](https://github.com/morluto/rea/commit/f4c99ab8a53806dd57dc24b3d5a600905bcb4686))
|
||||
* **artifacts:** name only host-available workflows for non-archive plists ([2295855](https://github.com/morluto/rea/commit/22958558eafdfccf71a7d5bbbcf360574a6b2e3c))
|
||||
* **artifacts:** recheck failed DMG detaches before reporting cleanup failure ([#1155](https://github.com/morluto/rea/issues/1155)) ([58a91f0](https://github.com/morluto/rea/commit/58a91f03d72162849991caf537ab058ccc918594))
|
||||
* **artifacts:** refuse unsupported extraction formats before scanning ([8e74c9c](https://github.com/morluto/rea/commit/8e74c9c62fb44556b90f20bb6c12771ac05a18f2))
|
||||
* **artifacts:** report encrypted archive entries as unsupported extraction ([de43da7](https://github.com/morluto/rea/commit/de43da71ae982dd0f3d7b5ae49c914729db3a6ec))
|
||||
* **artifacts:** report non-archive keyed-archive selections as unsupported targets ([fef38e4](https://github.com/morluto/rea/commit/fef38e4d22ee859ec43fb591d53e8b9973c69958))
|
||||
* **artifacts:** report non-archive keyed-archive selections as unsupported targets ([8c5088c](https://github.com/morluto/rea/commit/8c5088c8cdf32a9f96f6c479c6e70a71fa360168))
|
||||
* **artifacts:** report wrong target kinds as unsupported targets ([b34bdcd](https://github.com/morluto/rea/commit/b34bdcdb2877be394c59c9cbf70269dc1841ff78))
|
||||
* **artifacts:** report wrong target kinds as unsupported targets ([637bcb5](https://github.com/morluto/rea/commit/637bcb5e4182b6c60e922b18688642d8e1008e90))
|
||||
* **browser:** classify export-web-scripts capture selection failures by cause ([ddaa183](https://github.com/morluto/rea/commit/ddaa18301ab9c67e7a6f7ce66076075e8a4d5017))
|
||||
* **browser:** classify export-web-scripts capture selection failures by cause ([8c5e34c](https://github.com/morluto/rea/commit/8c5e34cd379392ca40f651ca5eeb6a947b44aa33))
|
||||
* **browser:** report Windows HAR heap exhaustion ([#1147](https://github.com/morluto/rea/issues/1147)) ([1393507](https://github.com/morluto/rea/commit/13935077ba039f6e342f9c11ab5eea935a335ae6))
|
||||
* **browser:** retain scenario observations across failures ([614e0c2](https://github.com/morluto/rea/commit/614e0c2e74819bb5e3ef3d2e16ff31ee6cab76ae))
|
||||
* **browser:** retain scenario resources until cleanup succeeds ([88640f6](https://github.com/morluto/rea/commit/88640f60fe566edbe2320688df62c7ef46c4c61c))
|
||||
* **ci:** pass selected environment to Windows Ghidra fixture ([#1281](https://github.com/morluto/rea/issues/1281)) ([f085589](https://github.com/morluto/rea/commit/f085589957922eaf421db814399e88e415712050))
|
||||
* **cli:** advertise defaulted inputs as optional in --schema and help ([#1175](https://github.com/morluto/rea/issues/1175)) ([b4927a0](https://github.com/morluto/rea/commit/b4927a0a4fa7e8935b101d214b396bfa78208281)), closes [#1169](https://github.com/morluto/rea/issues/1169)
|
||||
* **cli:** cancel routed JavaScript analysis in rea analyze ([#1156](https://github.com/morluto/rea/issues/1156)) ([089bbf5](https://github.com/morluto/rea/commit/089bbf58f3561e3f465c544bbcf8c307287b1256))
|
||||
* **cli:** complete cleanup after relayed interrupts ([#1202](https://github.com/morluto/rea/issues/1202)) ([911152a](https://github.com/morluto/rea/commit/911152a04d3aeb0d361141a8d54959009b269e49))
|
||||
* **cli:** list the read failure cause for an unreadable JSON input file ([db0afce](https://github.com/morluto/rea/commit/db0afce5f344268271c59acee04e96f986c5619a))
|
||||
* **cli:** list the read failure cause for an unreadable JSON input file ([6020df5](https://github.com/morluto/rea/commit/6020df53d9cd44845cf3b6b416db2d7802925578))
|
||||
* **cli:** resolve xrefs symbol selectors after address validation ([#1209](https://github.com/morluto/rea/issues/1209)) ([7e9c911](https://github.com/morluto/rea/commit/7e9c91199ecd133d26a51cb33b6e63e1f942df7e))
|
||||
* **config:** name the rejected setting and its constraint in configuration errors ([c3acc34](https://github.com/morluto/rea/commit/c3acc34475ca22387ea674839acfa1891b0b9f66))
|
||||
* **config:** name the rejected setting and its constraint in configuration errors ([0a0244d](https://github.com/morluto/rea/commit/0a0244db4212bc1f59dec4b925fa90b3b473136a)), closes [#1184](https://github.com/morluto/rea/issues/1184)
|
||||
* **doctor:** find the Homebrew Hopper cask without writing Homebrew caches ([98b07d0](https://github.com/morluto/rea/commit/98b07d07ddd20bea2c3bdf8d55ac2776a263bcf4))
|
||||
* **doctor:** find the Homebrew Hopper cask without writing Homebrew caches ([a503376](https://github.com/morluto/rea/commit/a503376b2f9d9ba14d1602e58e0e93059e9d9181)), closes [#1165](https://github.com/morluto/rea/issues/1165)
|
||||
* **domain:** report target read-permission denials as access_denied ([f36c10f](https://github.com/morluto/rea/commit/f36c10f9e8dc45a298d668e608f8e945bde487cf))
|
||||
* **domain:** report target read-permission denials as access_denied ([d0ff57a](https://github.com/morluto/rea/commit/d0ff57a45c8da8938e7ae16d00bfd5ec63310af3)), closes [#1188](https://github.com/morluto/rea/issues/1188)
|
||||
* **electron:** resolve effective preload after option overrides ([#1286](https://github.com/morluto/rea/issues/1286)) ([411498d](https://github.com/morluto/rea/commit/411498d45f860eba2fbc9d7051af20237b7a4cb1))
|
||||
* emit v-mode-compatible patterns for tool schemas ([#1220](https://github.com/morluto/rea/issues/1220)) ([f0da05f](https://github.com/morluto/rea/commit/f0da05f67fff1411cf2f7a48a752ee7b348c71ff))
|
||||
* **errors:** report caller-selected evidence path failures as invalid input ([d4a598e](https://github.com/morluto/rea/commit/d4a598e93649607dd293a9c81bbf79c211bc8dd4))
|
||||
* **errors:** report caller-selected evidence path failures as invalid input ([db177c9](https://github.com/morluto/rea/commit/db177c91b83adb0c3edc4f9f6f06e31ac7523d53))
|
||||
* **evidence:** cancel exports before atomic publication ([16624f1](https://github.com/morluto/rea/commit/16624f1324e156d14025b2c346596c9781c07488))
|
||||
* **ghidra:** classify modern Swift manglings in swift type analysis ([#1208](https://github.com/morluto/rea/issues/1208)) ([6935dc7](https://github.com/morluto/rea/commit/6935dc7fe1258b340bb9cdfb8cecae66ef7bc0d4))
|
||||
* **ghidra:** do not run Java from a relative JAVA_HOME in doctor and setup ([04a516b](https://github.com/morluto/rea/commit/04a516b1e369f7e57e08d0653cc08292ac497e07))
|
||||
* **ghidra:** do not run Java from a relative JAVA_HOME in doctor and setup ([c3f1698](https://github.com/morluto/rea/commit/c3f169823f690371ecfdd2d1b3b07a5749438cf1))
|
||||
* **ghidra:** keep and name a runtime directory that close could not remove ([5b769d3](https://github.com/morluto/rea/commit/5b769d3a781c645ed8885e4fabf01a00f0bbc89b))
|
||||
* **ghidra:** keep and name a runtime directory that close could not remove ([b79d379](https://github.com/morluto/rea/commit/b79d379c25bcc8ed61571c41a94b912b5704a067)), closes [#1190](https://github.com/morluto/rea/issues/1190)
|
||||
* **ghidra:** keep the NativeAOT target refusal's own recovery advice ([f198993](https://github.com/morluto/rea/commit/f198993f418e6aaa9755e7e6413abcb27c6cd684))
|
||||
* **ghidra:** preserve decoded return flow evidence ([b189c3c](https://github.com/morluto/rea/commit/b189c3ceb175dd9fa089eada9e9ca54e76307cb3))
|
||||
* **ghidra:** preserve decoded return flow evidence ([bcc8a77](https://github.com/morluto/rea/commit/bcc8a773e2a714e4899755b786a4fef1539bbace))
|
||||
* **ghidra:** reject relative installation and Java paths in doctor and setup ([8998438](https://github.com/morluto/rea/commit/8998438c23aa19e68f4ffa7574c5d341aa419165))
|
||||
* **ghidra:** reject relative installation and Java paths in doctor and setup ([5356856](https://github.com/morluto/rea/commit/53568568bf32b36274ba3a532f1e2cfb2daed271)), closes [#1183](https://github.com/morluto/rea/issues/1183)
|
||||
* **ghidra:** report NativeAOT host and target refusals as unsupported, not failures ([cfb49eb](https://github.com/morluto/rea/commit/cfb49eb8a88fc51f460907024c51e05a2f272502))
|
||||
* **ghidra:** report NativeAOT host and target refusals as unsupported, not failures ([42c276a](https://github.com/morluto/rea/commit/42c276af1ec02473a7b6700a31292aae9e53d53e)), closes [#1185](https://github.com/morluto/rea/issues/1185)
|
||||
* **ghidra:** resolve secondary symbols at function entries ([#1195](https://github.com/morluto/rea/issues/1195)) ([6fbdbd6](https://github.com/morluto/rea/commit/6fbdbd682038d012b04f0fe92c470cf1789fb101))
|
||||
* **ghidra:** stop dereferencing the removed error payload in address verification ([b80bd2d](https://github.com/morluto/rea/commit/b80bd2db966731fd3cfddfff7e92704053dbe955))
|
||||
* **ghidra:** stop dereferencing the removed error payload in address verification ([59fbaa2](https://github.com/morluto/rea/commit/59fbaa2d33359216534407f99f49705ac04a3426))
|
||||
* **ghidra:** use dot-free private session paths ([c3fc6c3](https://github.com/morluto/rea/commit/c3fc6c3aedc202e8ade015c4039a63e9f91cac6d))
|
||||
* **hopper:** keep the OS error when the FAT64 source cannot be reopened ([9f5dc52](https://github.com/morluto/rea/commit/9f5dc52be8420fb5e874595d63312bf80d07a3a2))
|
||||
* **hopper:** reserve Linux demo application before launch ([#1158](https://github.com/morluto/rea/issues/1158)) ([33d5ae0](https://github.com/morluto/rea/commit/33d5ae0b52b8684634ecf14d4e9469968c69d462))
|
||||
* **javascript:** allow cancellation during semantic graph commitment ([#1218](https://github.com/morluto/rea/issues/1218)) ([9405bd6](https://github.com/morluto/rea/commit/9405bd660def9946afaa5fcc1d99921a816911bf))
|
||||
* **javascript:** classify application input selection failures by cause ([fe5291e](https://github.com/morluto/rea/commit/fe5291e9166977a6ebbfa6ffcf7850af57192b34))
|
||||
* **javascript:** classify application input selection failures by cause ([0a14bbb](https://github.com/morluto/rea/commit/0a14bbbf05e0e61c98e06326fe7136e52fa4fb3c))
|
||||
* **javascript:** keep Wakaru version stderr out of the refusal message ([6489d58](https://github.com/morluto/rea/commit/6489d58851d055fd7ce621000d960bb42a0e4ce0))
|
||||
* **javascript:** match source-map originals that name parent directories ([#1215](https://github.com/morluto/rea/issues/1215)) ([889fb37](https://github.com/morluto/rea/commit/889fb374df0f8986f49e9d61b0fefd536327f781))
|
||||
* **javascript:** omit self-referential static references with disclosure ([#1157](https://github.com/morluto/rea/issues/1157)) ([b620e2a](https://github.com/morluto/rea/commit/b620e2a2bdcf3683b494d137bc409813735d5572))
|
||||
* **javascript:** preserve alias and container uncertainty ([73e98b2](https://github.com/morluto/rea/commit/73e98b2516dd792ea7b510aeed6b71f384bea21d))
|
||||
* **javascript:** preserve graphs with async chunk self-references ([#1152](https://github.com/morluto/rea/issues/1152)) ([7220166](https://github.com/morluto/rea/commit/72201668ed88c2cc822a07c08cf35a1e5ba88f35))
|
||||
* **javascript:** preserve property paths and uncertain slot presence ([475a0f8](https://github.com/morluto/rea/commit/475a0f84dac63d710e032bc7f731c1a3bda624ec))
|
||||
* **javascript:** reject encoded separators joined by HTML URL whitespace ([2ddd945](https://github.com/morluto/rea/commit/2ddd9456cd405f081784a4b3cb02cb77cd90fbd6))
|
||||
* **javascript:** reject encoded separators joined by HTML URL whitespace ([fb57c86](https://github.com/morluto/rea/commit/fb57c8643fb8b4733cbac9090ef58fd13a3b5347))
|
||||
* **javascript:** report located sources with differing digests as modified ([#1216](https://github.com/morluto/rea/issues/1216)) ([01174c4](https://github.com/morluto/rea/commit/01174c4a81fea109ae712ea1a746a83996f928ac))
|
||||
* **javascript:** resolve HTML script URLs after URL whitespace and percent decoding ([d601b80](https://github.com/morluto/rea/commit/d601b8016eaeb2955cea4e4c0aca10d17e7d12a7))
|
||||
* **javascript:** resolve HTML script URLs after URL whitespace and percent decoding ([502528c](https://github.com/morluto/rea/commit/502528c58df54c7b34eefdb5837f36e58f473405)), closes [#1236](https://github.com/morluto/rea/issues/1236)
|
||||
* **javascript:** resolve package exports targets only to the exact file ([#1181](https://github.com/morluto/rea/issues/1181)) ([2a620c0](https://github.com/morluto/rea/commit/2a620c06cdcaa32844c4a80aeb3df1793f1ce09e)), closes [#1174](https://github.com/morluto/rea/issues/1174)
|
||||
* keep the Ghidra session root free of dot-prefixed path elements ([a8b8288](https://github.com/morluto/rea/commit/a8b8288396cf2e053db768ef03f4db756aa8f71e))
|
||||
* **managed:** preserve literal library names during native matching ([81b70bc](https://github.com/morluto/rea/commit/81b70bc358031686c9d7afd049f21719c67a9525))
|
||||
* **managed:** separate metadata and signature completeness ([2f02eff](https://github.com/morluto/rea/commit/2f02eff9887fc715c33f5d8770a6e6191ba70ca9))
|
||||
* **mcp:** advertise input patterns without look-around ([1f099a6](https://github.com/morluto/rea/commit/1f099a6fc00b08fb0283dcabc5a238025d4c4c8b))
|
||||
* **mcp:** advertise input patterns without look-around ([a8701ea](https://github.com/morluto/rea/commit/a8701eaf489692449739d5b2490502b1a14951c5)), closes [#1282](https://github.com/morluto/rea/issues/1282)
|
||||
* **mcp:** deliver tool errors within advertised schema contracts ([da606ff](https://github.com/morluto/rea/commit/da606ff77718d9a3b10426fcf68c098138e6dcfa)), closes [#1261](https://github.com/morluto/rea/issues/1261)
|
||||
* **mcp:** deliver typed errors without invalid structured output ([#1277](https://github.com/morluto/rea/issues/1277)) ([7e96f25](https://github.com/morluto/rea/commit/7e96f25f8b1380a13a086a68f8d5a70380468151))
|
||||
* **mcp:** emit portable Unicode NUL patterns ([#1230](https://github.com/morluto/rea/issues/1230)) ([aa5750c](https://github.com/morluto/rea/commit/aa5750c263632d98d626c5ca3b985bb1519d2395))
|
||||
* **mcp:** measure tool errors against their text-only delivery ([fe34ec2](https://github.com/morluto/rea/commit/fe34ec2ef3e2968eeb06f64a1a0fdb8b825f56c4))
|
||||
* **mcp:** measure tool errors against their text-only delivery ([7b51121](https://github.com/morluto/rea/commit/7b51121f7cee21a5938f10ac3b5762e0bb41a58d))
|
||||
* **mcp:** only recommend exporting retained evidence ([5f749c2](https://github.com/morluto/rea/commit/5f749c2096af2617d4b6b387ff19c13001af8e54))
|
||||
* name host and tool requirements in optional adapter refusals ([68cb02f](https://github.com/morluto/rea/commit/68cb02fe8b123b9a0778c0065c13a15bcf0e577d))
|
||||
* name host and tool requirements in optional adapter refusals ([170d0bf](https://github.com/morluto/rea/commit/170d0bf9e2d45b2512ad556c02f31bdc81ea03ca))
|
||||
* **native:** distinguish batch selectors from resolved entries ([13c3b68](https://github.com/morluto/rea/commit/13c3b68730df80330fd867d921dea38770d5f880))
|
||||
* **native:** distinguish batch selectors from resolved entries ([bfaa5a6](https://github.com/morluto/rea/commit/bfaa5a6c2c0ddc9e6fcf16cd11ad06f24db174a2))
|
||||
* **native:** name the CLI flag that selects another plist ([9b69503](https://github.com/morluto/rea/commit/9b6950373cfaa1a2c0d28287bfadd4a421afb9e9))
|
||||
* **native:** name the plist selection that works when Info.plist is missing ([5612496](https://github.com/morluto/rea/commit/5612496f6d34a462dcd0fb69fa38337592e2f72d))
|
||||
* **native:** name the plist selection that works when Info.plist is missing ([a6fdf42](https://github.com/morluto/rea/commit/a6fdf426bd98c8cf7b7a7f589208d3193784062f))
|
||||
* **native:** refuse truncated executable headers at target admission ([d39a450](https://github.com/morluto/rea/commit/d39a450bdfac425962c3a6a6f47b45600057a230))
|
||||
* **native:** refuse truncated executable headers at target admission ([0b04d38](https://github.com/morluto/rea/commit/0b04d384cca4f22f271d60bcfe67f79cab89c8de)), closes [#1189](https://github.com/morluto/rea/issues/1189)
|
||||
* **process:** apply one timeout rule to both execFileOutput modes ([3d32819](https://github.com/morluto/rea/commit/3d328193d69a63a355644c51bfb70f524d99bca9))
|
||||
* **process:** await cleanup on CLI and MCP cancellation ([f6fe575](https://github.com/morluto/rea/commit/f6fe575094658e99ced78d45f385633fbfaaae32))
|
||||
* **process:** keep ports before a chunk-final period ([184ed97](https://github.com/morluto/rea/commit/184ed970b9021552a5454ced20d202d10cabcbb8))
|
||||
* **process:** localize capture truncation diagnostics ([#1284](https://github.com/morluto/rea/issues/1284)) ([33bf585](https://github.com/morluto/rea/commit/33bf585185ffcaea63d658e2d3145c900a680485))
|
||||
* **process:** normalize ports that end a sentence ([098d769](https://github.com/morluto/rea/commit/098d76900effdbc682b0f97bcde7f8711380ecea))
|
||||
* **process:** normalize ports that end a sentence ([db98a51](https://github.com/morluto/rea/commit/db98a51dd1750b09b3905e18efac526f7ebf90f7))
|
||||
* **process:** preserve numeric output and original PTY evidence ([c5c75c3](https://github.com/morluto/rea/commit/c5c75c38c3a4ff0153513b8f8ad600d8a5bacc09))
|
||||
* **process:** preserve numeric output and original PTY evidence ([00d8e44](https://github.com/morluto/rea/commit/00d8e44cbf82caa51709c2e0857fbaa69ea329b7))
|
||||
* **process:** preserve unknown filesystem absence in partial captures ([c8544b1](https://github.com/morluto/rea/commit/c8544b1574a32838da6d4ed40f2b72582edb60dd))
|
||||
* **process:** refuse malformed timeouts for stoppable commands ([136d4aa](https://github.com/morluto/rea/commit/136d4aab18d822f0f908096b8acfac556ef1873b))
|
||||
* **process:** refuse malformed timeouts for stoppable commands ([59c91c8](https://github.com/morluto/rea/commit/59c91c88fe88210b4b22b9f1365a16567513da4a))
|
||||
* **process:** require current captures and preserve cleanup failures ([2a77469](https://github.com/morluto/rea/commit/2a7746998d47446c4db6d4f524e0f183d6803f3f))
|
||||
* **process:** retain known events from incomplete captures ([5eb0049](https://github.com/morluto/rea/commit/5eb0049a9f502b404f30e48c3fe3430a942bb0b5))
|
||||
* **process:** stop cancelled Swift helper builds before removing their files ([cf41561](https://github.com/morluto/rea/commit/cf415619cadcfcb18cda59a54b26c9d164f750a4))
|
||||
* **process:** stop cancelled Swift helper builds before removing their files ([c717204](https://github.com/morluto/rea/commit/c7172047951e7dbb9df58b57ff1b43ac9bd47909)), closes [#1173](https://github.com/morluto/rea/issues/1173)
|
||||
* **process:** stop timed-out commands with the caller's stop signal ([4b38d18](https://github.com/morluto/rea/commit/4b38d1899566089d19b9fd137202f7d86b62fc6f))
|
||||
* **process:** stop timed-out commands with the caller's stop signal ([063de50](https://github.com/morluto/rea/commit/063de5014f19e85c38b6bec5984de4f38e71adaa))
|
||||
* **process:** treat non-ASCII letters as port continuations ([ad813e4](https://github.com/morluto/rea/commit/ad813e473b094ab0d3ab30798b7d252e2f4beb94))
|
||||
* **process:** validate terminal byte totals against retained frames ([69833fb](https://github.com/morluto/rea/commit/69833fb0d0fe9b320b5745a992822342f2c54a92))
|
||||
* **process:** validate terminal byte totals against retained frames ([39eb058](https://github.com/morluto/rea/commit/39eb0587c95ad028bb961c539ad1f326a1e8d81c))
|
||||
* **providers:** preserve observations and primary cleanup failures ([0696bde](https://github.com/morluto/rea/commit/0696bde2660bb5d6538d8c01da15aa023312aa9f))
|
||||
* **reference:** represent unreadable symlink targets explicitly ([b7b4333](https://github.com/morluto/rea/commit/b7b433370310c3ffe33d5027d8393151d5e862da))
|
||||
* restore caching, preserve failures, and verify fixture claims ([#1211](https://github.com/morluto/rea/issues/1211)) ([27a9cea](https://github.com/morluto/rea/commit/27a9ceade7232c1c9a24dc7d454ae060948183a1))
|
||||
* **server:** report 2025-era client identity and features in binary_session ([e6abaef](https://github.com/morluto/rea/commit/e6abaefbbc2bd81a6be34cbe71b87be801158c16))
|
||||
* **server:** report 2025-era client identity and features in binary_session ([b8312fa](https://github.com/morluto/rea/commit/b8312fa8d184ba9f422d1f5592fb41e49811544a)), closes [#1167](https://github.com/morluto/rea/issues/1167)
|
||||
* **session:** require an open target for session-bound native and artifact tools ([d8e314c](https://github.com/morluto/rea/commit/d8e314cb24e7a4a494347e45474234e0aad74ad1))
|
||||
* **session:** require an open target for session-bound native and artifact tools ([cf77d07](https://github.com/morluto/rea/commit/cf77d073fc06ab95ce4181c30d68b1346e990b64)), closes [#1166](https://github.com/morluto/rea/issues/1166)
|
||||
* **session:** retain failed provider cleanup for lifecycle retries ([#1260](https://github.com/morluto/rea/issues/1260)) ([24a3aa0](https://github.com/morluto/rea/commit/24a3aa08d1f69e25b441f086300ba3082d860a35))
|
||||
* **setup:** keep Codex TOML comments when adding or removing REA ([f5539cf](https://github.com/morluto/rea/commit/f5539cfc2f6c0daf6c275fc97f3351e2cf89b2c3))
|
||||
* **setup:** keep Codex TOML comments when adding or removing REA ([e9c48a6](https://github.com/morluto/rea/commit/e9c48a61e4c2c05c5b77ef4a815c5b2a65a9337e)), closes [#1163](https://github.com/morluto/rea/issues/1163)
|
||||
* **setup:** preserve initial backups and Codex comments ([#1204](https://github.com/morluto/rea/issues/1204)) ([6051cb2](https://github.com/morluto/rea/commit/6051cb2c0c723d88dc43ee5ef8a96971bf84d49e))
|
||||
* **setup:** stop remaining uninstall removals after a client fails ([fd975c7](https://github.com/morluto/rea/commit/fd975c75fa353840e6e887232ca68496cf8705ea))
|
||||
* **setup:** stop uninstall before removals when a client config is unsafe ([4fb2f3e](https://github.com/morluto/rea/commit/4fb2f3e6ed0233505eada3cddeeff0ea0bfc6fe5))
|
||||
* **setup:** stop uninstall before removals when a client config is unsafe ([5cc841d](https://github.com/morluto/rea/commit/5cc841d5c154f692bdc257e106cc4b212dc19dc0)), closes [#1164](https://github.com/morluto/rea/issues/1164)
|
||||
* **snapshot:** report query workflow and Evidence counts separately ([04aef6d](https://github.com/morluto/rea/commit/04aef6df331634238cfc61f320ba4a4e720750a4))
|
||||
* **snapshot:** report query workflow and Evidence counts separately ([0e77953](https://github.com/morluto/rea/commit/0e779533a9108a550b4d10dcb3920f91c7d48989))
|
||||
* **website:** clarify Android host and JDK requirements ([5691606](https://github.com/morluto/rea/commit/5691606aa948bddc641e3ecbccf5375a12b65924))
|
||||
* **website:** identify publication verifier requests ([e6905c1](https://github.com/morluto/rea/commit/e6905c18e09c41e04525fb1a84575483df789204))
|
||||
* **windows:** retain runtime authority after cleanup failure ([af7a439](https://github.com/morluto/rea/commit/af7a43963351c655eb86197bf52ae798a04e9c22))
|
||||
* **windows:** retain unsettled jobs for cleanup retry ([76b214e](https://github.com/morluto/rea/commit/76b214e45fa05fe723e216894327185b9b4e6f53))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **mcp:** share repeated output schema definitions. ([441813c](https://github.com/morluto/rea/commit/441813c0708dbee82ef554c91e973ae02964bf2e))
|
||||
|
||||
* **hopper:** resolve string search records once per query ([#1229](https://github.com/morluto/rea/issues/1229)) ([9426c3f](https://github.com/morluto/rea/commit/9426c3f1bbb94711d8b33097e604b9b8ed853b5e))
|
||||
* **mcp:** reuse input schemas without losing caller guidance ([#1162](https://github.com/morluto/rea/issues/1162)) ([e1afbc1](https://github.com/morluto/rea/commit/e1afbc1914cc7ff90265313e942f34ea4a58a4fc))
|
||||
* **snapshot:** avoid redundant result copies and serialization ([552634d](https://github.com/morluto/rea/commit/552634d634655a722b2204cfbc4b29f6d442c294))
|
||||
* **snapshot:** avoid redundant result copies and serialization ([569d2bc](https://github.com/morluto/rea/commit/569d2bc6b9f600114267a4c69f8400393ea1bc2f))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **domain:** remove obsolete comparison representations ([5510b27](https://github.com/morluto/rea/commit/5510b27d7f863a3df26212fba4a59f8df0da3712))
|
||||
* enforce canonical evidence and provider boundaries ([63bf914](https://github.com/morluto/rea/commit/63bf91405cf97b114a84f337fb1272d98a267999))
|
||||
* remove forwarding exports and redundant aliases ([ad59531](https://github.com/morluto/rea/commit/ad59531a960a0c51ef6857fa1391270dda0fa7a4))
|
||||
* **schemas:** share portable identifier and base64 patterns ([#1227](https://github.com/morluto/rea/issues/1227)) ([954badb](https://github.com/morluto/rea/commit/954badbaaadd24a210190ce3015e40c23d327567))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* add French README translation ([84094af](https://github.com/morluto/rea/commit/84094af54a4e0a6247445e59abff27bc4eca361b))
|
||||
* add French README translation ([9664b2b](https://github.com/morluto/rea/commit/9664b2b101fe4ac334b2911f9210b50d58d90a89))
|
||||
* celebrate 30,000 GitHub stars 🎉 ([6987cb0](https://github.com/morluto/rea/commit/6987cb066a5aa5e51480bb06a93a758e6f67cd1e))
|
||||
* celebrate 30,000 GitHub stars 🎉 ([beca997](https://github.com/morluto/rea/commit/beca9977eb2b18fc7b9844cff0ca70004b6ff21a))
|
||||
* clarify REA has no affiliated cryptocurrency or token ([#1214](https://github.com/morluto/rea/issues/1214)) ([c2d8146](https://github.com/morluto/rea/commit/c2d8146f94c7533eb87681c2405e93bdfef21b59))
|
||||
* explain 6.2 contract and saved-data migration ([#1304](https://github.com/morluto/rea/issues/1304)) ([c267e99](https://github.com/morluto/rea/commit/c267e9922751c03df312655ebd623de1b480215c))
|
||||
* **ghidra:** explain supported heap and CPU controls ([#1278](https://github.com/morluto/rea/issues/1278)) ([0c6852b](https://github.com/morluto/rea/commit/0c6852b392b34319a6fd2c6dffcac10cee51ab94)), closes [#1255](https://github.com/morluto/rea/issues/1255)
|
||||
* keep the 30k milestone in Star History ([#1217](https://github.com/morluto/rea/issues/1217)) ([c399670](https://github.com/morluto/rea/commit/c399670653884673158f314ed38260aef74da593))
|
||||
* require focused brittleness cleanup and executable examples ([d6832c3](https://github.com/morluto/rea/commit/d6832c3632847cf3e62d4082b4b8b8d9af554633))
|
||||
* thank feature request contributors ([9db1fd0](https://github.com/morluto/rea/commit/9db1fd0bd7b17ed1dd21b9c72dabfa4ec6c23a3f))
|
||||
* **website:** continue the Calculator example with reconstruction ([4578647](https://github.com/morluto/rea/commit/4578647c59a7487bce597a65be6eab8be0412e84))
|
||||
* **website:** continue the Calculator example with reconstruction ([18a1e8c](https://github.com/morluto/rea/commit/18a1e8cd0efe14c06fd14aabe0c318d2ae390f78))
|
||||
* **website:** explain SEO maintenance and indexing follow-up ([bb8ff2c](https://github.com/morluto/rea/commit/bb8ff2cfba638ee3a77f951c01e98340c757f525))
|
||||
* **website:** introduce the worked homepage examples ([f6f5c16](https://github.com/morluto/rea/commit/f6f5c160979624cdcb33928f1a77cf000de403d4))
|
||||
* **website:** invite deeper projects and reader feedback ([3fb9ffa](https://github.com/morluto/rea/commit/3fb9ffaa4d67685412873cbd3101c64fabbeaff3))
|
||||
|
||||
|
||||
### Tests
|
||||
|
||||
* **apple:** verify compiled Mach-O root occurrence facts ([#1300](https://github.com/morluto/rea/issues/1300)) ([4bb4c6f](https://github.com/morluto/rea/commit/4bb4c6fe6d34525c7665a9d38b584b026dd5cdd9))
|
||||
* **artifacts:** assert asset-catalog target refusal only on macOS ([44d5b4b](https://github.com/morluto/rea/commit/44d5b4b25e0831769c1bd4e5426ef498d0bd3c74))
|
||||
* **artifacts:** pin per-occurrence roles for direct Mach-O roots and shared bytes ([701d90a](https://github.com/morluto/rea/commit/701d90aba48aaf4f23e1a73e46ebf9968fb85293))
|
||||
* **artifacts:** pin per-occurrence roles for direct Mach-O roots and shared bytes ([b9e29c5](https://github.com/morluto/rea/commit/b9e29c52d483704c64cd88f87eaa117066288602)), closes [#1234](https://github.com/morluto/rea/issues/1234) [#1235](https://github.com/morluto/rea/issues/1235)
|
||||
* **artifacts:** read /bin/ls slice layout from its FAT table ([#1223](https://github.com/morluto/rea/issues/1223)) ([c89b3a0](https://github.com/morluto/rea/commit/c89b3a00dce8df36ddb696b59c80e1adc6aceac1))
|
||||
* **artifacts:** verify compiled Mach-O roles and permissions ([4bb4c6f](https://github.com/morluto/rea/commit/4bb4c6fe6d34525c7665a9d38b584b026dd5cdd9))
|
||||
* consolidate redundant assertions into boundary workflows ([b71a591](https://github.com/morluto/rea/commit/b71a591a60e3fa5333889e3cf97e05f4ae03c686))
|
||||
* **ghidra:** preserve selected verification resource limits ([1c3766a](https://github.com/morluto/rea/commit/1c3766a8bcc8a2b6af679cdaa92a73fec42b3372))
|
||||
* **ghidra:** size oversized annotations for canonical delivery ([1e34197](https://github.com/morluto/rea/commit/1e341973cfad1dbf06755f3a086ffc23d42a723c))
|
||||
* **ghidra:** use host-native absolute session root fixtures ([1e83d27](https://github.com/morluto/rea/commit/1e83d270c2098b1bcb59591698a257a05d2296fc))
|
||||
* **javascript:** expect the invalid input_path code in the streamed CLI failure test ([d186ca7](https://github.com/morluto/rea/commit/d186ca7e41928fc2748de2631f5cb43115776571))
|
||||
* **mcp:** read bundle failures from public error content ([69c3b32](https://github.com/morluto/rea/commit/69c3b32a21c90ea96194fb695f7b58992e8b3714))
|
||||
* **mcp:** read typed errors in canonical Evidence regressions ([fd46cff](https://github.com/morluto/rea/commit/fd46cff017f0110aaab1216764cf01a71b341766))
|
||||
* **mcp:** validate public error diagnostics in acceptance workflows ([09058eb](https://github.com/morluto/rea/commit/09058ebc4a68c76d865a92dbddb65811ba8344f7))
|
||||
* **native:** assert batch identity in production registration ([933fcb3](https://github.com/morluto/rea/commit/933fcb33c1fa1eae109228b89c1200abb7745eb8))
|
||||
* **native:** build the .app fixture executable with a complete Mach-O header ([facaae8](https://github.com/morluto/rea/commit/facaae865af243656ab8f557aeb42f5909748f2b))
|
||||
* **process:** check malformed capture choices concurrently ([6020a0b](https://github.com/morluto/rea/commit/6020a0b3683ce16e1441963473605f870c247e29))
|
||||
* **process:** check malformed capture choices concurrently ([48de11c](https://github.com/morluto/rea/commit/48de11ce2e5af9810fc97d1c2f89df56bf3a3d4d))
|
||||
* remove duplicate workflow examples and self assertions ([d6d1cd2](https://github.com/morluto/rea/commit/d6d1cd248506015b59a2abdbbb84e3dbd02388fc))
|
||||
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
* **website:** allow explicitly requested Pages-only publication ([796b65e](https://github.com/morluto/rea/commit/796b65e1cd15f3cf819396716320db007655ee3f))
|
||||
* **website:** allow explicitly requested Pages-only publication ([864160d](https://github.com/morluto/rea/commit/864160d8858fad3c0afebff81c31b090a1ebf823))
|
||||
* **website:** allow explicitly requested Pages-only publication ([#1205](https://github.com/morluto/rea/issues/1205)) ([796b65e](https://github.com/morluto/rea/commit/796b65e1cd15f3cf819396716320db007655ee3f))
|
||||
* **website:** publish Cloudflare and Pages from one artifact ([761881c](https://github.com/morluto/rea/commit/761881cec33b18d7228bae5e2093e5d44b96ceb1))
|
||||
|
||||
## [6.1.0](https://github.com/morluto/rea/compare/rea-agents-6.0.0...rea-agents-6.1.0) (2026-10-09)
|
||||
|
||||
|
||||
@@ -159,6 +402,7 @@
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **mcp:** advertise each tool JSON Schema once per target ([#1059](https://github.com/morluto/rea/issues/1059)) ([5ccb924](https://github.com/morluto/rea/commit/5ccb9241be15bb6a19b99fbbfb27ff3356ba157b))
|
||||
* **build:** avoid duplicate documentation generation ([c701376](https://github.com/morluto/rea/commit/c701376777212ec9acfd10c97f45b1eaf0c885c6))
|
||||
* **javascript:** index configuration and module lookups ([67252b2](https://github.com/morluto/rea/commit/67252b23b9e35e9526fed7842dab47a2c7c9fe5f))
|
||||
* **javascript:** reuse validated immutable graphs ([#1112](https://github.com/morluto/rea/issues/1112)) ([ec6db0f](https://github.com/morluto/rea/commit/ec6db0f0630c4b0b35f0db84c0030784c1aa58a6))
|
||||
|
||||
+24
-6
@@ -42,9 +42,10 @@ npm run check:fast
|
||||
|
||||
`npm ci` installs the exact dependencies and prepares the Husky hooks without
|
||||
building the project. Run `npm run build:cached` when you need the standalone
|
||||
CLI or MCP server; `npm run build` remains the uncached compiler leaf used by
|
||||
Turbo. Turbo caches deterministic builds and static checks across Git
|
||||
worktrees. After a package, lockfile, or managed-skill version change, run
|
||||
CLI or MCP server; it compiles the runtime and bundles the authored skill.
|
||||
`npm run build` forces those tasks to rerun. Neither command generates test
|
||||
catalogs, documentation projections, or managed verification evidence. Turbo
|
||||
caches deterministic builds and static checks across Git worktrees. After a package, lockfile, or managed-skill version change, run
|
||||
`npm run metadata:generate` before building.
|
||||
|
||||
Keep dependencies flowing inward through the existing domain, contracts, provider, application, server, and adapter layers. Parse unknown values at process and protocol boundaries, model expected failures with `Result`, and preserve the canonical tool inventory defined by `TOOL_CONTRACTS` unless a deliberate contract change updates every verifier, generated catalog artifact, and snapshot. Keep tool discovery complete and report capability- and session-scoped availability through `binary_session`.
|
||||
@@ -96,7 +97,22 @@ 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.
|
||||
|
||||
`check:fast` runs cached typecheck and lint, reporting diagnostics on failure.
|
||||
`check:fast` runs cached source/test typecheck and lint without compiling or
|
||||
running generators. `npm run test:prepare` prepares all artifacts consumed by
|
||||
the complete suite; focused tests prepare only their runtime and declared
|
||||
artifact dependencies. The test-only MCP catalog lives at
|
||||
`.cache/mcp-tool-catalog.json` and can be regenerated independently with
|
||||
`npm run mcp-catalog:generate`.
|
||||
|
||||
Build and generation commands hold one SQLite exclusive transaction around
|
||||
their whole Turbo graph, including cache restoration. The persistent database
|
||||
in `.cache/rea-command-locks/` is a lock identity, not a stale-file marker;
|
||||
the operating system releases ownership when the command supervisor exits.
|
||||
On POSIX, that supervisor retains the transaction and process-group identity
|
||||
through cancellation, including loss of its parent runner. Windows cancellation
|
||||
waits for the command to complete. Do not remove the database while commands
|
||||
are running.
|
||||
|
||||
`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
|
||||
@@ -104,8 +120,10 @@ the same configuration. Pre-commit formats and lints staged files; pre-push runs
|
||||
`docs:check` builds and validates generated metadata for the current checkout.
|
||||
The product catalog (`docs/public/product-catalog.json`), portable managed
|
||||
conformance projections (`docs/verification/managed-conformance-*.json`), and
|
||||
packaged skill (`skills/`) are ignored build outputs. Edit skill instructions
|
||||
and references in `skill-src/`; the build adds catalog-dependent metadata to
|
||||
packaged skill (`skills/`) are ignored generated outputs. `docs:generate`
|
||||
prepares documentation and managed evidence; `evidence:generate` prepares only
|
||||
the managed commitment and its runtime/skill dependencies. Edit skill
|
||||
instructions and references in `skill-src/`; the skill task adds metadata to
|
||||
the packaged copy without rewriting authored files. The generated manifest
|
||||
commits to that exact packaged skill bundle. It is a portable projection of
|
||||
the deterministic managed verifier, not a record of optional real-provider runs.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
**English** · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
**English** · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA: Reverse Engineer Anything
|
||||
|
||||
@@ -157,7 +157,7 @@ Additional tools and host support depend on the target:
|
||||
| Websites | Page structure, scripts, network observations and requested screenshots | A Chrome-family browser; [browser analysis](https://rea.tools/guides/browser/) |
|
||||
| Saved network captures | Requests, responses, exposed payloads and source locations | HAR; mitmdump on Linux for native mitmproxy captures; [capture guide](docs/web-network-captures.md) |
|
||||
| .NET assemblies | Metadata, CIL instructions, declared native dependencies and build comparisons | Static inspection; [managed-code guide](docs/managed-code-analysis.md) |
|
||||
| Android APKs | Manifest declarations, classes, decompiled methods and references | Headless JADX and a full JDK on Linux/macOS; [Android guide](docs/android-analysis.md) |
|
||||
| Android APKs | Manifest declarations, classes, decompiled methods and references | Headless JADX and a full JDK on Linux/macOS/Windows x64; [Android guide](docs/android-analysis.md) |
|
||||
| Firmware | Regions, extraction results and native-analysis handoffs | Binwalk / Unblob on Linux; [firmware guide](docs/firmware-analysis.md) |
|
||||
| Packages and resources | File inventories, digests, plists, Apple bundle anatomy and extracted resources | [Artifact and JavaScript guide](docs/javascript-artifact-reconstruction.md), [Apple applications](docs/apple-application-analysis.md) |
|
||||
| Process behavior | Terminal output, interactions, exit and filesystem observations, and run comparisons | Linux/macOS with a native PTY; [process capture](docs/process-capture.md) |
|
||||
@@ -298,7 +298,7 @@ For exact options, prerequisites and result contracts:
|
||||
- [Readiness and troubleshooting](docs/installation.md#check-readiness-for-your-task): diagnose one agent or analysis engine.
|
||||
- [CLI and Evidence](docs/cli.md): commands, provider selection, snapshots, import/export and exit statuses.
|
||||
- [MCP contracts](docs/mcp-contracts.md) and [agent prompts](docs/mcp-prompts.md): tool results, sessions and guided investigations.
|
||||
- [Tool catalog](docs/mcp-contracts.md#generated-catalog): build-generated inventory of tools, providers and CLI commands.
|
||||
- [Tool catalog](docs/mcp-contracts.md#generated-catalog): generated inventory of tools, providers and CLI commands.
|
||||
- [Roadmap](docs/roadmap.md): planned work and capability trackers.
|
||||
|
||||
Report vulnerabilities through [SECURITY.md](SECURITY.md).
|
||||
@@ -319,9 +319,9 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and checks,
|
||||
|
||||
## Star history
|
||||
|
||||
🎉 **20,000 GitHub stars — thank you!**
|
||||
🎉 **30,000 GitHub stars — thank you!**
|
||||
|
||||
Thanks to everyone using REA, reporting bugs, testing builds, and contributing fixes.
|
||||
Thanks to everyone using REA, reporting bugs, requesting features, testing builds, and contributing fixes.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
@@ -335,6 +335,8 @@ Thanks to everyone using REA, reporting bugs, testing builds, and contributing f
|
||||
|
||||
REA provides tools for lawful reverse-engineering research, analysis, and reconstruction. You are responsible for obtaining any required authorization and complying with applicable laws. The project does not endorse illegal or unauthorized use.
|
||||
|
||||
REA is an open-source software project. We have not issued or endorsed any cryptocurrency or token. Tokens using the REA name are not affiliated with the project.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · **العربية** · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · **العربية** · [فارسی](README_fa.md)
|
||||
|
||||
# REA: هندسة عكسية لأي شيء
|
||||
|
||||
@@ -266,9 +266,9 @@ npx rea-agents@latest setup
|
||||
|
||||
## سجل النجوم
|
||||
|
||||
🎉 **20,000 نجمة على GitHub — شكرًا لكم!**
|
||||
🎉 **30,000 نجمة على GitHub — شكرًا لكم!**
|
||||
|
||||
شكرًا لكل من يستخدم REA، ويبلّغ عن الأخطاء، ويختبر إصدارات البناء، ويساهم بالإصلاحات.
|
||||
شكرًا لكل من يستخدم REA، ويبلّغ عن الأخطاء، ويقترح ميزات، ويختبر إصدارات البناء، ويساهم بالإصلاحات.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · **Deutsch** · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · **Deutsch** · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA: Alles reverse-engineeren
|
||||
|
||||
@@ -264,9 +264,9 @@ Die Entwicklungsumgebung und Prüfungen beschreibt [CONTRIBUTING.md](CONTRIBUTIN
|
||||
|
||||
## Sternverlauf
|
||||
|
||||
🎉 **20.000 GitHub-Sterne – vielen Dank!**
|
||||
🎉 **30.000 GitHub-Sterne – vielen Dank!**
|
||||
|
||||
Danke an alle, die REA nutzen, Fehler melden, Builds testen und Fehlerbehebungen beitragen.
|
||||
Danke an alle, die REA nutzen, Fehler melden, Funktionswünsche äußern, Builds testen und Fehlerbehebungen beitragen.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · **Español** · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · **Español** · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA: Ingeniería inversa de cualquier cosa
|
||||
|
||||
@@ -264,9 +264,9 @@ Consulta [CONTRIBUTING.md](CONTRIBUTING.md) para preparar el entorno de desarrol
|
||||
|
||||
## Historial de estrellas
|
||||
|
||||
🎉 **20.000 estrellas en GitHub: ¡gracias!**
|
||||
🎉 **30.000 estrellas en GitHub: ¡gracias!**
|
||||
|
||||
Gracias a quienes usan REA, informan de errores, prueban compilaciones y contribuyen con correcciones.
|
||||
Gracias a quienes usan REA, informan de errores, solicitan funciones, prueban compilaciones y contribuyen con correcciones.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
<div align="center" dir="rtl">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · **فارسی**
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · **فارسی**
|
||||
|
||||
# REA: مهندسی معکوس هر چیزی
|
||||
|
||||
@@ -249,9 +249,9 @@ npx rea-agents@latest setup
|
||||
|
||||
## تاریخچهٔ ستارهها
|
||||
|
||||
🎉 **۲۰٬۰۰۰ ستارهٔ GitHub — سپاسگزاریم!**
|
||||
🎉 **۳۰٬۰۰۰ ستارهٔ GitHub — سپاسگزاریم!**
|
||||
|
||||
از همهٔ کسانی که از REA استفاده میکنند، خطاها را گزارش میدهند، buildها را آزمایش میکنند و در بهبود پروژه مشارکت دارند سپاسگزاریم.
|
||||
از همهٔ کسانی که از REA استفاده میکنند، خطاها را گزارش میدهند، قابلیتهای جدید پیشنهاد میکنند، buildها را آزمایش میکنند و در بهبود پروژه مشارکت دارند سپاسگزاریم.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date"><picture><source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date&theme=dark&legend=top-left" /><source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date" /><img alt="تاریخچهٔ ستارههای GitHub پروژهٔ REA" src="https://api.star-history.com/chart?repos=morluto/rea&type=date" /></picture></a>
|
||||
|
||||
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · **Français** · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA : rétro-ingénierie de tout
|
||||
|
||||
### Un seul MCP pour la rétro-ingénierie des binaires, des applications et du comportement à l'exécution.
|
||||
|
||||
**Repérez une fonctionnalité qui vous plaît. Comprenez son fonctionnement, jusqu'au niveau du binaire.**
|
||||
|
||||
[](https://www.npmjs.com/package/rea-agents)
|
||||
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
||||
[](docs/mcp-contracts.md#generated-catalog)
|
||||
[](https://nodejs.org/)
|
||||
[](https://skills.sh/morluto/rea/reverse-engineer-anything)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/GkcryMnJDM)
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[Site web](https://rea.tools/) · [Guides](https://rea.tools/guides/) · [Études de cas](https://rea.tools/showcase/)**
|
||||
|
||||
[Démarrage rapide](#démarrage-rapide) · [Comment fonctionne REA](#comment-fonctionne-rea) · [Ce que vous pouvez analyser](#ce-que-vous-pouvez-analyser) · [Études de cas](#études-de-cas) · [Questions fréquentes](#questions-fréquentes) · [Documentation](#documentation)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
<br />
|
||||
|
||||
<img src="docs/assets/rea-hopper-analysis.png" alt="REA lance son pont d'analyse dans Hopper pendant l'inspection d'un binaire natif" 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>Rejoignez la communauté de rétro-ingénierie</strong>
|
||||
</a><br />
|
||||
<sub>Discord · Questions et réponses · Partage de projets</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
Vous avez repéré dans une application une fonctionnalité que vous aimeriez avoir dans votre propre produit ? Demandez à votre agent de l'étudier avec REA. Il peut inspecter l'application sans son code source, expliquer le fonctionnement de la fonctionnalité, présenter les preuves et en construire une version pour votre projet.
|
||||
|
||||
REA connecte votre agent à des outils d'inspection de binaires natifs, d'applications JavaScript et Electron, d'assemblys .NET et de sites web. Les mêmes outils sont aussi disponibles depuis votre terminal. L'analyse s'exécute localement, et chaque conclusion s'accompagne des preuves et des limites sur lesquelles elle repose.
|
||||
|
||||
Setup enregistre REA auprès de votre agent et installe les instructions de workflow correspondantes. L'analyse native peut utiliser une installation existante de Hopper ou de Ghidra ; Setup peut aussi installer Hopper après votre accord. L'analyse statique JavaScript ne nécessite aucun de ces moteurs.
|
||||
|
||||
> **[Visitez le site web de REA](https://rea.tools/)** pour les instructions d'installation, des guides illustrés et des études de cas réelles.
|
||||
|
||||
## Démarrage rapide
|
||||
|
||||
### Configurer votre agent
|
||||
|
||||
Une fois Node.js et npm installés, exécutez :
|
||||
|
||||
```bash
|
||||
npx rea-agents setup
|
||||
```
|
||||
|
||||
Choisissez vos agents, vérifiez les modifications proposées et approuvez-les. Setup ajoute le serveur MCP de REA et les instructions de workflow correspondantes, et sauvegarde la configuration existante. Redémarrez ensuite votre agent.
|
||||
|
||||
Setup prend en charge Claude Code, Codex, Cursor, Gemini CLI, Grok Build et [d'autres agents](docs/installation.md#supported-agents). Consultez [Installation et configuration](docs/installation.md) pour la configuration des fournisseurs et l'enregistrement MCP manuel.
|
||||
|
||||
### Interroger votre agent
|
||||
|
||||
```text
|
||||
Comprends comment fonctionne la recherche dans l'app Notes, montre-moi les preuves
|
||||
et construis une fonctionnalité similaire pour mon projet.
|
||||
```
|
||||
|
||||
Remplacez Notes par votre application cible et la fonctionnalité que vous voulez comprendre.
|
||||
|
||||
### Utiliser le terminal
|
||||
|
||||
Inspectez le répertoire extrait ou l'archive ASAR d'une application JavaScript/Electron :
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
Le résultat comprend les modules, les imports, les frontières Electron et les preuves associées. Remplacez le chemin par votre cible, par exemple `"D:/apps/example"` sous Windows.
|
||||
|
||||
Pour installer la commande `rea` pour un usage régulier :
|
||||
|
||||
```bash
|
||||
npm install --global rea-agents
|
||||
rea --help
|
||||
```
|
||||
|
||||
Pour l'analyse native, configurez d'abord un fournisseur. Consultez le [guide CLI et Evidence](docs/cli.md) pour les commandes natives, le choix du fournisseur, les snapshots et l'utilisation dans des scripts.
|
||||
|
||||
### Mettre à jour REA
|
||||
|
||||
REA évolue rapidement, et les nouvelles versions apportent fréquemment des corrections de bugs. Maintenez votre installation à jour.
|
||||
|
||||
Pour une CLI installée avec npm :
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Pour actualiser les enregistrements de vos agents et le skill, exécutez la commande setup affichée par la mise à jour.
|
||||
|
||||
Si vous utilisez `npx`, mettez à jour la configuration de votre agent avec :
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
Vérifiez les modifications proposées par Setup et redémarrez votre agent. Pour des commandes CLI ponctuelles, utilisez `npx rea-agents@latest` suivi de la commande.
|
||||
|
||||
## Comment fonctionne REA
|
||||
|
||||
Votre agent appelle REA via MCP pour inspecter la cible et suivre le code pertinent. REA renvoie ses constatations avec les preuves correspondantes. L'agent s'en sert pour poser des questions complémentaires, expliquer le comportement, ou écrire et tester une implémentation. Les commandes CLI utilisent les mêmes workflows.
|
||||
|
||||

|
||||
|
||||
[Ouvrir la figure en taille réelle](website/public/assets/figures/rea-investigation-flow.svg).
|
||||
|
||||
<a id="current-status"></a>
|
||||
|
||||
## Ce que vous pouvez analyser
|
||||
|
||||
REA nécessite Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, ainsi que npm. Les outils supplémentaires et les hôtes pris en charge dépendent de la cible :
|
||||
|
||||
| Cible | Ce que REA renvoie | Prérequis et guide |
|
||||
| ---------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Binaires natifs | Pseudo-code, assembleur, chaînes, symboles, appels et références | Hopper, Ghidra ou IDA ; [analyse native](https://rea.tools/guides/native/) |
|
||||
| Structure ELF hors ligne | Sections, segments, symboles et relocations d'origine, et protections statiques candidates | pwntools fourni par l'utilisateur sous Linux x64 ; [diagnostics binaires](docs/binary-diagnostics.md) |
|
||||
| Bytecode EVM | Sélecteurs de dispatch, offsets en octets, arguments inférés et mutabilité | Fichier local brut ou hexadécimal ; [guide EVM hors ligne](docs/evm-bytecode.md) |
|
||||
| Crashs Linux enregistrés | Notes brutes, registres et signaux de chaque thread enregistré, et correspondances de mappings facultatives | pwntools fourni par l'utilisateur ; GDB/pwndbg en option ; [crashs enregistrés](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | Modules, imports, source maps, routes, IPC et relations avec les modules natifs | Node.js et npm ; [analyse d'applications](https://rea.tools/guides/javascript/) |
|
||||
| Sites web | Structure des pages, scripts, observations réseau et captures d'écran demandées | Un navigateur de la famille Chrome ; [analyse du navigateur](https://rea.tools/guides/browser/) |
|
||||
| Captures réseau enregistrées | Requêtes, réponses, payloads exposés et emplacements dans le code source | HAR ; mitmdump sous Linux pour les captures mitmproxy natives ; [guide des captures](docs/web-network-captures.md) |
|
||||
| Assemblys .NET | Métadonnées, instructions CIL, dépendances natives déclarées et comparaisons de builds | Inspection statique ; [guide du code managé](docs/managed-code-analysis.md) |
|
||||
| APK Android | Déclarations du manifeste, classes, méthodes décompilées et références | JADX headless et un JDK complet sous Linux/macOS ; [guide Android](docs/android-analysis.md) |
|
||||
| Firmware | Régions, résultats d'extraction et passage de relais vers l'analyse native | Binwalk / Unblob sous Linux ; [guide firmware](docs/firmware-analysis.md) |
|
||||
| Paquets et ressources | Inventaires de fichiers, empreintes, plists, anatomie des bundles Apple et ressources extraites | [Guide artefacts et JavaScript](docs/javascript-artifact-reconstruction.md), [applications Apple](docs/apple-application-analysis.md) |
|
||||
| Comportement des processus | Sortie du terminal, interactions, code de sortie, observations du système de fichiers et comparaisons | Linux/macOS avec un PTY natif ; [capture de processus](docs/process-capture.md) |
|
||||
|
||||
L'inspection statique JavaScript et .NET lit les fichiers fournis sans exécuter l'application. La capture à l'exécution lance la cible choisie ou interagit avec elle avec vos permissions utilisateur ; chaque guide d'exécution décrit ses effets.
|
||||
|
||||
<a id="choosing-a-deep-analysis-provider"></a>
|
||||
|
||||
Les formats natifs et les hôtes pris en charge varient selon le fournisseur. Consultez [la configuration de Hopper et Ghidra](docs/installation.md#hopper), le [guide IDA](docs/ida-provider.md) et [la prise en charge expérimentale de Ghidra sous Windows](docs/windows-ghidra-p0.md). Ghidra prend aussi en charge [l'analyse DOS 16 bits](docs/ghidra-dos.md). Pour les gros binaires, augmentez son délai de démarrage avec `REA_GHIDRA_STARTUP_TIMEOUT_MS`. Pour le choix du fournisseur, consultez le [guide CLI](docs/cli.md#choose-a-provider). Vérifiez la [disponibilité dans les versions publiées](docs/installation.md#released-package-and-main) pour les fonctionnalités ajoutées depuis la dernière version npm.
|
||||
|
||||
## Études de cas
|
||||
|
||||
### DX-Ball : reconstruire un calcul de panoramique sonore
|
||||
|
||||
Suivez un appel sonore jusqu'à la fonction qui convertit une position en panoramique, inspectez ses instructions et transformez un pseudo-code incomplet en C. La reconstruction réussit 3 205 cas issus du x86 d'origine et reproduit à l'identique les 63 octets de la fonction compilée.
|
||||
|
||||
[Lire l'étude de cas](https://rea.tools/showcase/dx-ball/) · [Dépôt de la reconstruction](https://github.com/N0zoM1z0/dx-ball)
|
||||
|
||||
### Notion : suivre le pont du presse-papiers d'Electron
|
||||
|
||||
Trouvez l'API de presse-papiers du renderer, suivez-la à travers le preload et l'IPC jusqu'au processus principal, puis inspectez le format de presse-papiers enrichi.
|
||||
|
||||
[Lire l'étude de cas](https://rea.tools/showcase/notion/)
|
||||
|
||||
### TH04 : retrouver un calcul d'anneau de projectiles sous DOS
|
||||
|
||||
Inspectez les instructions 16 bits du jeu PC-98 d'origine, retrouvez les calculs d'angles fixes et visés, et comparez le C++ reconstruit avec la sortie du compilateur d'époque.
|
||||
|
||||
[Lire l'étude de cas](https://rea.tools/showcase/th04/) · [Dépôt de la reconstruction](https://github.com/N0zoM1z0/th04)
|
||||
|
||||
Si vous avez utilisé REA sur un projet intéressant, nous serions ravis de le découvrir. Partagez votre cas dans une [issue](https://github.com/morluto/rea/issues) ou une [pull request](https://github.com/morluto/rea/pulls), en précisant la cible, votre question, la manière dont REA vous a aidé et ce que vous avez découvert.
|
||||
|
||||
## Questions fréquentes
|
||||
|
||||
<details>
|
||||
<summary><strong>Quels agents peuvent utiliser REA ?</strong></summary>
|
||||
|
||||
Tout agent compatible avec les serveurs MCP locaux. Setup configure les [agents pris en charge](docs/installation.md#supported-agents) ; les autres clients peuvent utiliser [l'enregistrement MCP manuel](docs/installation.md#mcp-registry).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Ai-je besoin de Hopper, Ghidra ou IDA ?</strong></summary>
|
||||
|
||||
L'analyse native approfondie utilise l'un d'eux. L'inspection statique JavaScript et .NET fonctionne sans moteur d'analyse native. Setup peut installer Hopper après votre accord ; Ghidra et IDA utilisent vos installations existantes. Consultez [la configuration des fournisseurs](docs/installation.md#hopper).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Dois-je lancer Hopper au préalable ?</strong></summary>
|
||||
|
||||
REA lance Hopper lorsqu'une opération en a besoin. Sous macOS, une boîte de dialogue de premier lancement peut vous demander de choisir le mode démo ou d'activer votre licence. Consultez [Démarrage et dépannage de Hopper](docs/installation.md#launcher-paths-and-troubleshooting).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Que fait l'installation du skill depuis skills.sh ?</strong></summary>
|
||||
|
||||
Le skill fournit des instructions d'investigation à votre agent. Utilisez `rea setup` pour enregistrer le serveur MCP de REA et installer les instructions correspondantes, puis redémarrez votre agent. Consultez [l'installation du skill seul](docs/installation.md#skill-only-installation).
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Quel code REA renvoie-t-il ?</strong></summary>
|
||||
|
||||
L'analyse native renvoie du pseudo-code et de l'assembleur. L'analyse JavaScript/Electron récupère les modules et leurs relations. Votre agent s'appuie sur ces résultats pour écrire et tester une implémentation ; les [études de cas](#études-de-cas) en donnent des exemples concrets.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>REA envoie-t-il mon application en ligne ?</strong></summary>
|
||||
|
||||
REA analyse les cibles localement. Votre agent reçoit les résultats des outils, et son fournisseur de modèle applique sa propre politique de données.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Que faire si je rencontre un bug ?</strong></summary>
|
||||
|
||||
Commencez par mettre à jour ; une version récente l'a peut-être déjà corrigé.
|
||||
|
||||
Pour une CLI installée avec npm :
|
||||
|
||||
```bash
|
||||
rea update
|
||||
```
|
||||
|
||||
Pour une configuration d'agent via `npx` :
|
||||
|
||||
```bash
|
||||
npx rea-agents@latest setup
|
||||
```
|
||||
|
||||
Si vous utilisez un agent, terminez l'[actualisation de la configuration](#mettre-à-jour-rea) et redémarrez-le. Relancez la même tâche. Si le problème persiste, [ouvrez une issue](https://github.com/morluto/rea/issues) en indiquant votre version de REA, le type de cible, les étapes pour reproduire le problème et la sortie d'erreur.
|
||||
|
||||
</details>
|
||||
|
||||
## Documentation
|
||||
|
||||
Commencez par les [guides pas à pas](https://rea.tools/guides/) du site web. Pour les options exactes, les prérequis et les contrats de résultats :
|
||||
|
||||
- [Installation et configuration](docs/installation.md) : enregistrement des agents, configuration des fournisseurs, mises à jour et désinstallation.
|
||||
- [Vérification et dépannage](docs/installation.md#check-readiness-for-your-task) : diagnostiquer un agent ou un moteur d'analyse précis.
|
||||
- [CLI et Evidence](docs/cli.md) : commandes, choix du fournisseur, snapshots, import/export et codes de sortie.
|
||||
- [Contrats MCP](docs/mcp-contracts.md) et [prompts pour agents](docs/mcp-prompts.md) : résultats des outils, sessions et investigations guidées.
|
||||
- [Catalogue des outils](docs/mcp-contracts.md#generated-catalog) : inventaire des outils, fournisseurs et commandes CLI, généré lors du build.
|
||||
- [Feuille de route](docs/roadmap.md) : travaux prévus et suivi des capacités.
|
||||
|
||||
Signalez les vulnérabilités via [SECURITY.md](SECURITY.md).
|
||||
|
||||
## Contribuer
|
||||
|
||||
Votre aide sur REA est la bienvenue ! [Ouvrez une issue](https://github.com/morluto/rea/issues) pour signaler un bug ou proposer une fonctionnalité, ou [envoyez une pull request](https://github.com/morluto/rea/pulls) pour améliorer le code ou la documentation.
|
||||
|
||||
Consultez [CONTRIBUTING.md](CONTRIBUTING.md) pour l'environnement de développement et les vérifications, la page [tests](docs/testing.md) pour les filières de vérification, et la [carte de l'architecture](docs/architecture.mermaid) pour la structure du projet.
|
||||
|
||||
## Liens du projet
|
||||
|
||||
[Site web](https://rea.tools/) · [npm](https://www.npmjs.com/package/rea-agents) · [skills.sh](https://skills.sh/morluto/rea/reverse-engineer-anything) · [Issues](https://github.com/morluto/rea/issues) · [Sécurité](SECURITY.md)
|
||||
|
||||
## Historique des étoiles
|
||||
|
||||
🎉 **30 000 étoiles GitHub — merci !**
|
||||
|
||||
Merci à toutes les personnes qui utilisent REA, signalent des bugs, proposent des fonctionnalités, testent les builds et contribuent des correctifs.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
<img alt="Historique des étoiles GitHub de REA" src="https://api.star-history.com/chart?repos=morluto/rea&type=date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Avertissement
|
||||
|
||||
REA fournit des outils destinés à la recherche, à l'analyse et à la reconstruction légales en rétro-ingénierie. Il vous appartient d'obtenir toute autorisation nécessaire et de respecter les lois applicables. Le projet n'encourage aucune utilisation illégale ou non autorisée.
|
||||
|
||||
## Licence
|
||||
|
||||
[MIT](LICENSE)
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · **日本語** · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · **日本語** · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA:あらゆるものをリバースエンジニアリング
|
||||
|
||||
@@ -266,9 +266,9 @@ REA への貢献を歓迎します · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · **한국어** · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · **한국어** · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA: 무엇이든 리버스 엔지니어링
|
||||
|
||||
@@ -266,9 +266,9 @@ REA에 도움을 보태 주세요! [issue를 등록](https://github.com/morluto/
|
||||
|
||||
## 스타 기록
|
||||
|
||||
🎉 **GitHub 스타 20,000개, 감사합니다!**
|
||||
🎉 **GitHub 스타 30,000개, 감사합니다!**
|
||||
|
||||
REA를 사용하고, 버그를 보고하고, 빌드를 테스트하고, 수정에 기여해 주신 모든 분께 감사드립니다.
|
||||
REA를 사용하고, 버그를 보고하고, 기능을 요청하고, 빌드를 테스트하고, 수정에 기여해 주신 모든 분께 감사드립니다.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · **Polski** · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · **Polski** · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA: Reverse Engineer Anything
|
||||
|
||||
@@ -264,9 +264,9 @@ Zobacz [CONTRIBUTING.md](CONTRIBUTING.md), aby poznać konfigurację środowiska
|
||||
|
||||
## Historia gwiazdek
|
||||
|
||||
🎉 **20 000 gwiazdek na GitHubie — dziękujemy!**
|
||||
🎉 **30 000 gwiazdek na GitHubie — dziękujemy!**
|
||||
|
||||
Dziękujemy wszystkim, którzy używają REA, zgłaszają błędy, testują kompilacje i przesyłają poprawki.
|
||||
Dziękujemy wszystkim, którzy używają REA, zgłaszają błędy, proponują nowe funkcje, testują kompilacje i przesyłają poprawki.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · **Português (Brasil)** · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · **Português (Brasil)** · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA: Faça engenharia reversa de qualquer coisa
|
||||
|
||||
@@ -264,9 +264,9 @@ Consulte [CONTRIBUTING.md](CONTRIBUTING.md) para configurar o ambiente de desenv
|
||||
|
||||
## Histórico de estrelas
|
||||
|
||||
🎉 **20.000 estrelas no GitHub — muito obrigado!**
|
||||
🎉 **30.000 estrelas no GitHub — muito obrigado!**
|
||||
|
||||
Obrigado a todos que usam o REA, relatam bugs, testam builds e contribuem com correções.
|
||||
Obrigado a todos que usam o REA, relatam bugs, sugerem funcionalidades, testam builds e contribuem com correções.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · **Русский** · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · **Русский** · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA: Реверс-инжиниринг чего угодно
|
||||
|
||||
@@ -264,9 +264,9 @@ npx rea-agents@latest setup
|
||||
|
||||
## История звёзд
|
||||
|
||||
🎉 **20 000 звёзд на GitHub — спасибо!**
|
||||
🎉 **30 000 звёзд на GitHub — спасибо!**
|
||||
|
||||
Спасибо всем, кто использует REA, сообщает об ошибках, тестирует сборки и помогает с исправлениями.
|
||||
Спасибо всем, кто использует REA, сообщает об ошибках, предлагает новые функции, тестирует сборки и помогает с исправлениями.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · **ไทย** · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · **ไทย** · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA: Reverse Engineer Anything
|
||||
|
||||
@@ -264,9 +264,9 @@ npx rea-agents@latest setup
|
||||
|
||||
## ประวัติดาว
|
||||
|
||||
🎉 **20,000 ดาวบน GitHub — ขอบคุณทุกคน!**
|
||||
🎉 **30,000 ดาวบน GitHub — ขอบคุณทุกคน!**
|
||||
|
||||
ขอบคุณทุกคนที่ใช้ REA รายงานบั๊ก ทดสอบบิลด์ และช่วยแก้ไข
|
||||
ขอบคุณทุกคนที่ใช้ REA รายงานบั๊ก เสนอฟีเจอร์ ทดสอบบิลด์ และช่วยแก้ไข
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · **Türkçe** · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · **Türkçe** · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA: Her şeyi tersine mühendislikle inceleyin
|
||||
|
||||
@@ -264,9 +264,9 @@ Geliştirme ortamı ve kontroller için [CONTRIBUTING.md](CONTRIBUTING.md), doğ
|
||||
|
||||
## Yıldız geçmişi
|
||||
|
||||
🎉 **GitHub'da 20.000 yıldız — teşekkürler!**
|
||||
🎉 **GitHub'da 30.000 yıldız — teşekkürler!**
|
||||
|
||||
REA'yı kullanan, hata bildiren, derlemeleri test eden ve düzeltmelere katkıda bulunan herkese teşekkürler.
|
||||
REA'yı kullanan, hata bildiren, özellik talep eden, derlemeleri test eden ve düzeltmelere katkıda bulunan herkese teşekkürler.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · **Українська** · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · **Українська** · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA: Реверс-інжиніринг будь-чого
|
||||
|
||||
@@ -264,9 +264,9 @@ npx rea-agents@latest setup
|
||||
|
||||
## Історія зірок
|
||||
|
||||
🎉 **20 000 зірок на GitHub — дякуємо!**
|
||||
🎉 **30 000 зірок на GitHub — дякуємо!**
|
||||
|
||||
Дякуємо всім, хто використовує REA, повідомляє про помилки, тестує збірки й допомагає з виправленнями.
|
||||
Дякуємо всім, хто використовує REA, повідомляє про помилки, пропонує нові функції, тестує збірки й допомагає з виправленнями.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · **Tiếng Việt** · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · **Tiếng Việt** · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA: Kỹ thuật dịch ngược cho mọi thứ
|
||||
|
||||
@@ -264,9 +264,9 @@ Xem [CONTRIBUTING.md](CONTRIBUTING.md) về thiết lập môi trường phát t
|
||||
|
||||
## Lịch sử sao
|
||||
|
||||
🎉 **20.000 sao trên GitHub — xin cảm ơn!**
|
||||
🎉 **30.000 sao trên GitHub — xin cảm ơn!**
|
||||
|
||||
Cảm ơn mọi người đã sử dụng REA, báo lỗi, kiểm thử bản dựng và đóng góp bản sửa lỗi.
|
||||
Cảm ơn mọi người đã sử dụng REA, báo lỗi, đề xuất tính năng, kiểm thử bản dựng và đóng góp bản sửa lỗi.
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · [简体中文](README_zh.md) · **繁體中文** · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · [简体中文](README_zh.md) · **繁體中文** · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA:逆向分析一切
|
||||
|
||||
@@ -266,9 +266,9 @@ npx rea-agents@latest setup
|
||||
|
||||
## Star 歷史
|
||||
|
||||
🎉 **GitHub Star 達到 20,000 個,感謝大家!**
|
||||
🎉 **GitHub Star 達到 30,000 個,感謝大家!**
|
||||
|
||||
感謝每一位使用 REA、報告錯誤、測試建置和貢獻修復的朋友。
|
||||
感謝每一位使用 REA、報告錯誤、提出功能需求、測試建置和貢獻修復的朋友。
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) · **简体中文** · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
[English](README.md) · **简体中文** · [繁體中文](README_zh-TW.md) · [日本語](README_ja.md) · [한국어](README_ko.md) · [Türkçe](README_tr.md) · [Русский](README_ru.md) · [Tiếng Việt](README_vi.md) · [ไทย](README_th.md) · [Deutsch](README_de.md) · [Español](README_es.md) · [Français](README_fr.md) · [Українська](README_uk.md) · [Polski](README_pl.md) · [Português (Brasil)](README_pt-BR.md) · [العربية](README_ar.md) · [فارسی](README_fa.md)
|
||||
|
||||
# REA:逆向分析一切
|
||||
|
||||
@@ -266,9 +266,9 @@ npx rea-agents@latest setup
|
||||
|
||||
## Star 历史
|
||||
|
||||
🎉 **GitHub Star 达到 20,000 个,感谢大家!**
|
||||
🎉 **GitHub Star 达到 30,000 个,感谢大家!**
|
||||
|
||||
感谢每一位使用 REA、报告错误、测试构建和贡献修复的朋友。
|
||||
感谢每一位使用 REA、报告错误、提出功能需求、测试构建和贡献修复的朋友。
|
||||
|
||||
<a href="https://www.star-history.com/?repos=morluto%2Frea&type=date">
|
||||
<picture>
|
||||
|
||||
@@ -866,6 +866,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
JsonArray operands = new JsonArray();
|
||||
JsonArray references = new JsonArray();
|
||||
JsonArray destinations = new JsonArray();
|
||||
JsonArray flowEvidence = new JsonArray();
|
||||
JsonObject flow = new JsonObject();
|
||||
flow.addProperty("kind", "unavailable");
|
||||
flow.addProperty("conditional", false);
|
||||
@@ -881,7 +882,20 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
result.addProperty("mnemonic", instruction.getMnemonicString());
|
||||
result.addProperty("raw_disassembly", instruction.toString());
|
||||
RefType type = instruction.getFlowType();
|
||||
flow.addProperty("kind", type.isCall() ? "call" : type.isJump() ? "jump" : type.isTerminal() ? "terminal" : "fallthrough");
|
||||
JsonObject listingFlow = new JsonObject();
|
||||
listingFlow.addProperty("source", "ghidra-listing-flow-type");
|
||||
listingFlow.addProperty("value", type.getName());
|
||||
flowEvidence.add(listingFlow);
|
||||
boolean decodedReturn = false;
|
||||
for (PcodeOp operation : instruction.getPcode()) {
|
||||
if (operation.getOpcode() != PcodeOp.RETURN) continue;
|
||||
decodedReturn = true;
|
||||
JsonObject returnEvidence = new JsonObject();
|
||||
returnEvidence.addProperty("source", "ghidra-instruction-pcode");
|
||||
returnEvidence.addProperty("value", operation.getMnemonic());
|
||||
flowEvidence.add(returnEvidence);
|
||||
}
|
||||
flow.addProperty("kind", type.isCall() ? "call" : type.isJump() ? "jump" : decodedReturn ? "return" : type.isTerminal() ? "terminal" : "fallthrough");
|
||||
flow.addProperty("conditional", type.isConditional());
|
||||
flow.addProperty("computed", type.isComputed());
|
||||
if (!type.isComputed()) for (Address target : instruction.getFlows()) destinations.add(canonicalAddress(target));
|
||||
@@ -917,10 +931,13 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
}
|
||||
}
|
||||
flow.add("direct_destinations", destinations);
|
||||
flow.add("classification_evidence", flowEvidence);
|
||||
result.add("operands", operands);
|
||||
result.add("flow", flow);
|
||||
result.add("references", references);
|
||||
result.add("limitations", GSON.toJsonTree(List.of("Operand components are Ghidra Listing decoder tokens; effective memory base/index/displacement roles and per-instruction context mode are unavailable. The mode field is the program language variant.", "Terminal flow is not classified as a return without provider evidence.")));
|
||||
List<String> limitations = new ArrayList<>(List.of("Operand components are Ghidra Listing decoder tokens; effective memory base/index/displacement roles and per-instruction context mode are unavailable. The mode field is the program language variant."));
|
||||
if (flow.get("kind").getAsString().equals("terminal")) limitations.add("No RETURN operation was observed in the provider's instruction p-code; terminal classification remains generic.");
|
||||
result.add("limitations", GSON.toJsonTree(limitations));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1629,9 +1646,20 @@ public final class ReaGhidraBridge extends HeadlessScript {
|
||||
Function function = entry.function;
|
||||
Symbol symbol = function.getSymbol();
|
||||
String qualified = symbol == null ? function.getName() : symbol.getName(true);
|
||||
if (function.getName().equals(value) || qualified.equals(value)) {
|
||||
matches.add(function);
|
||||
boolean matchesName = function.getName().equals(value) || qualified.equals(value);
|
||||
if (!matchesName) {
|
||||
// Imported labels (for example Mach-O _main) can share a
|
||||
// function entry without becoming its primary symbol. Only
|
||||
// exact entries qualify; an interior label is not a selector.
|
||||
for (Symbol entrySymbol : currentProgram.getSymbolTable().getSymbols(function.getEntryPoint())) {
|
||||
monitor.checkCancelled();
|
||||
if (entrySymbol.getName().equals(value) || entrySymbol.getName(true).equals(value)) {
|
||||
matchesName = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (matchesName) matches.add(function);
|
||||
}
|
||||
if (matches.size() > 1)
|
||||
throw new RequestFailure("ambiguous", "Ghidra procedure name is ambiguous: " + value +
|
||||
|
||||
@@ -553,7 +553,12 @@ def _search_results(document, kind, params):
|
||||
if not matches(item[1]):
|
||||
continue
|
||||
selected.append(item)
|
||||
return [{"address": address, **_string_inventory(document)[address]} for address, _ in selected] if kind == "string" else [
|
||||
if not selected:
|
||||
return []
|
||||
if kind == "string":
|
||||
records = _string_inventory(document)
|
||||
return [{"address": address, **records[address]} for address, _ in selected]
|
||||
return [
|
||||
{
|
||||
"address": address,
|
||||
"value": value,
|
||||
|
||||
@@ -252,7 +252,7 @@ No request can cross a concurrent target or provider transition.
|
||||
canonical path, source digest, generic kind and format, selected architecture,
|
||||
and available architectures. Hopper loader arguments leave the domain model.
|
||||
Provider-owned databases use a generic analysis-database classification; their
|
||||
adapter determines support. A legacy `.hop` target is therefore accepted only
|
||||
adapter determines support. A `.hop` analysis database is therefore accepted only
|
||||
by Hopper without teaching the domain how to open it. Ghidra v1 continues to use
|
||||
private temporary projects and does not open user projects.
|
||||
|
||||
@@ -336,19 +336,19 @@ entries, so snapshots must be recaptured before they can be used as a cache.
|
||||
An embedded Evidence bundle can still be imported separately through the
|
||||
Evidence import flow; it is never promoted into an analysis cache.
|
||||
|
||||
### 8. Add profile commitments to new deep Evidence while preserving legacy records
|
||||
### 8. Commit concrete or explicitly unknown analysis profiles in Evidence
|
||||
|
||||
The optional `analysis_profile` field uses the canonical envelope above. Every
|
||||
newly created deep-analysis observation must include it, and its provider ID
|
||||
and version must match the record's provider. The field participates in the
|
||||
Evidence semantic ID.
|
||||
Every Evidence record includes `analysis_profile`, either the canonical profile
|
||||
envelope above or `null` when no profile was observed. Deep-analysis observations
|
||||
commit the resolved provider ID, version, and effective configuration in this
|
||||
profile. The field participates in the Evidence semantic ID.
|
||||
|
||||
Existing Evidence records without `analysis_profile` remain valid and keep
|
||||
their existing IDs. Absence on a legacy deep record means the analysis profile
|
||||
is unknown; REA does not infer or enrich it during import. Such evidence can be
|
||||
displayed and cited but cannot establish profile compatibility, seed snapshot
|
||||
replay, or support an "unchanged" conclusion that depends on identical analysis
|
||||
semantics.
|
||||
An explicitly unknown profile can be displayed and cited, but cannot establish
|
||||
profile compatibility, seed snapshot replay, or support an "unchanged"
|
||||
conclusion that depends on identical analysis semantics. Snapshot query entries
|
||||
must have matching profile-bound Evidence; imports reject missing or mismatched
|
||||
observations instead of silently dropping query entries. Both `entries` and
|
||||
`workflow_entries` are required arrays, including when empty.
|
||||
|
||||
Different providers or profile digests are never snapshot-compatible. A
|
||||
derived comparison may compare cross-provider normalized facts only when its
|
||||
@@ -407,12 +407,12 @@ means no dynamic provider has supplied a snapshot. Once one or more start,
|
||||
snapshots are not live inventories, and a point-in-time observation never proves
|
||||
historical absence.
|
||||
|
||||
The existing flat `provider`, `providers`, and `capabilities` status fields stay
|
||||
during the 1.x compatibility window. Their effective deep-operation entries
|
||||
come only from the active binding; target-free ambiguity is unavailable rather
|
||||
than arbitrarily collapsed. The new candidate fields are authoritative for
|
||||
selection. A future major release may remove the synthetic composite provider
|
||||
field after callers have migrated.
|
||||
Status exposes `analysis_provider_candidates` for configured deep providers and
|
||||
`analysis_provider_binding` for the selected concrete provider and profile.
|
||||
`capabilities` retains effective operation availability and exact provider
|
||||
provenance, including auxiliary operation families. There are no synthetic
|
||||
flat `provider` or `providers` aliases. Provider prompt completion uses the
|
||||
actual selectable candidates.
|
||||
|
||||
Selection failures extend the tagged `ProviderSelectionError` with a stable
|
||||
reason:
|
||||
@@ -448,26 +448,20 @@ snapshot's provider.
|
||||
- Tool names and normalized output contracts remain provider-neutral. REA does
|
||||
not add `ghidra_decompile` or `hopper_decompile` tools.
|
||||
|
||||
## Compatibility and migration
|
||||
## Provider and snapshot representations
|
||||
|
||||
| Existing behavior or data | Decision |
|
||||
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| No provider selector, only Hopper usable | `auto` selects the sole candidate; behavior remains equivalent |
|
||||
| No deep provider usable | Automatic open may remain unbound for auxiliary operations; deep capabilities explain why they are unavailable |
|
||||
| Hopper and Ghidra both usable, no preference | Open fails with `ambiguous`; installation order never becomes policy |
|
||||
| Existing Hopper environment settings | Remain supported as Hopper adapter configuration |
|
||||
| `open_binary` and CLI structured `loaderArgs` output | Retained as a deprecated Hopper-only compatibility projection for 1.x; the analysis profile is authoritative |
|
||||
| `BinaryTarget.loaderArgs` | Removed; Hopper derives them inside its adapter and commits normalized semantics in the profile |
|
||||
| Legacy `.hop` input | Classified generically as an analysis database and accepted only by Hopper |
|
||||
| Snapshot v1 | Not accepted as the current snapshot format; recapture before cache replay |
|
||||
| Existing Evidence without a profile | Accepted unchanged; profile compatibility is unknown |
|
||||
| New deep-analysis Evidence | Must include `analysis_profile`; its semantic ID commits the field |
|
||||
| Existing status fields | Retained for 1.x with additive binding and candidate fields |
|
||||
| Provider failure after selection | Returned from the selected provider; no transparent retry through another engine |
|
||||
|
||||
The deprecated `loaderArgs` response must never be read to select a provider or
|
||||
validate a snapshot. It is empty for non-Hopper bindings and may be removed only
|
||||
with a separately documented major contract change.
|
||||
| Behavior or data | Contract |
|
||||
| -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
|
||||
| No selector, one usable deep provider | `auto` selects the sole candidate |
|
||||
| No usable deep provider | Automatic open may remain unbound for auxiliary operations; deep capabilities explain their unavailability |
|
||||
| Several usable deep providers without a preference | Open fails with `ambiguous`; installation order never becomes policy |
|
||||
| Hopper environment settings | Adapter configuration committed into the typed analysis profile, including effective loader arguments |
|
||||
| `open_binary` output | Provider-neutral target identity only; no `loaderArgs` projection |
|
||||
| `.hop` input | Analysis database accepted only by Hopper |
|
||||
| Snapshots | Concrete provider/profile binding, required primitive and workflow entry arrays, and matching authenticated Evidence |
|
||||
| Evidence without an observed profile | Explicit `analysis_profile: null`; ineligible for profile-dependent replay |
|
||||
| Snapshot save receipt | `primitive_entries`, `workflow_entries`, and `evidence_records` counts without a duplicate `entries` alias |
|
||||
| Provider failure after selection | Returned from that provider, without transparent retry through another engine |
|
||||
|
||||
## Required behavior scenarios
|
||||
|
||||
|
||||
@@ -23,10 +23,37 @@ export REA_JADX_MCP_JAR=/absolute/path/jadx-headless-mcp-0.7.1-all.jar
|
||||
export JAVA_HOME=/absolute/path/existing-jdk
|
||||
```
|
||||
|
||||
On Windows x64, use a REA package with its matching bundled Windows native
|
||||
controls **including owned protocol stdin support**. This Windows boundary is
|
||||
new on repository main; an older released package can still reject this host.
|
||||
PowerShell accepts paths containing spaces without additional escaping
|
||||
inside these environment values:
|
||||
|
||||
```powershell
|
||||
$env:REA_JADX_MCP_JAR = 'D:\tools\jadx-headless-mcp-0.7.1-all.jar'
|
||||
$env:JAVA_HOME = 'D:\tools\jdk-21'
|
||||
rea inspect-android-package 'D:\targets\Example.apk'
|
||||
```
|
||||
|
||||
REA selects `JAVA_HOME\bin\java.exe`, or resolves `java.exe` on the selected
|
||||
Windows `PATH`. A missing or mismatched native artifact reports the package
|
||||
prerequisite rather than attempting an unowned Java launch. Windows arm64 is
|
||||
outside this provider's current native boundary.
|
||||
|
||||
Readiness checks the selected JAR's ZIP directory for the classes consumed by
|
||||
REA's metadata bridge, as well as Java's compiler and version modules. It does
|
||||
not load an APK or execute engine code. Invalid archives and missing bridge
|
||||
classes report JAR-specific recovery; classpath inventory alone does not prove
|
||||
that every class or engine operation will load successfully. Discovery limits
|
||||
each ZIP metadata read to 8 MiB before allocation; a larger directory reports
|
||||
the exhausted read budget and the selected JAR path.
|
||||
|
||||
The current metadata bridge is verified on macOS arm64 with OpenJDK 21 and the
|
||||
public Appium ApiDemos fixture. The POSIX adapter also supports Linux; the new
|
||||
bridge has not yet undergone real verification on Linux/JDK 25.
|
||||
Windows is unsupported for this provider's owned stdio process boundary.
|
||||
Windows x64 uses an owned Job Object with bidirectional stdio, atomic job
|
||||
assignment and kill-on-owner-close. Real Windows verification uses JDK 21 and
|
||||
the pinned JADX 0.7.1 JAR; see the real verification lane below.
|
||||
|
||||
## Answer an analyst question
|
||||
|
||||
@@ -130,6 +157,12 @@ target/configuration changes, failure, timeout, cancellation, MCP disconnect or
|
||||
server shutdown. One-shot CLI commands always join cleanup before returning.
|
||||
REA never writes to the original APK or launches target code.
|
||||
|
||||
On Windows, stdin writes preserve stream backpressure and run outside the Node
|
||||
event loop. Cancellation terminates the retained job, including descendants,
|
||||
before workspace cleanup. An abrupt CLI/server process termination closes the
|
||||
job and stops Java, but cannot run JavaScript workspace cleanup; temporary
|
||||
workspace files can remain after that forced exit.
|
||||
|
||||
If Java cannot start the bridge, REA reports the selected executable and retained
|
||||
startup diagnostics. Select a full JDK through `JAVA_HOME` or `PATH`, and use
|
||||
`java --list-modules` to confirm `jdk.compiler` is present. A runnable JRE alone
|
||||
@@ -189,6 +222,12 @@ without the compiler module. It checks real Java startup failures, missing
|
||||
selectors, recovery after failures, and CLI cancellation with owned cleanup.
|
||||
The original APK and host Java configuration remain unchanged.
|
||||
|
||||
The same lane runs on Windows x64 with the bundled native controls and a full
|
||||
JDK. It checks CLI/MCP results, real MCP cancellation and disconnect, and Java
|
||||
exit after abrupt CLI owner termination. Windows forced termination is checked
|
||||
separately from the POSIX CLI SIGTERM handler; it does not claim a structured
|
||||
cancellation result or workspace removal after the owner is forcibly killed.
|
||||
|
||||
The download script verifies fixed SHA-256 values, reuses matching files and
|
||||
refuses to overwrite a different existing file. APK/JAR files live under ignored
|
||||
`_reference/` and are excluded from package contents. The lane exercises actual
|
||||
|
||||
@@ -31,6 +31,14 @@ overall coverage becomes `partial` and a limitation explains the omission.
|
||||
Duplicate inventory pages are merged by artifact identity and occurrence path;
|
||||
their Evidence IDs remain attached as source Evidence.
|
||||
|
||||
Content nodes identify bytes by SHA-256 and report only content-derived formats.
|
||||
Each occurrence reports its own `artifact_kind`, `artifact_format`, and executable
|
||||
permission. Identical bytes can therefore appear as a script, resource, or native
|
||||
addon at different paths without overwriting another occurrence's role. Package
|
||||
formats such as APK and IPA belong to the occurrence; their ZIP bytes share a
|
||||
content format. Graph and directory identities use locale-independent Unicode
|
||||
ordering.
|
||||
|
||||
For IPA inventories, archive components outside every application root remain
|
||||
in the component lists. Their JavaScript and native candidates are grouped with
|
||||
other unrooted components, preserving the inventory projection's path pairing
|
||||
|
||||
@@ -84,6 +84,13 @@ uses isolated mode and an owned cache. Exact upstream profiles are recorded in
|
||||
stack can accompany an unknown (`null`) NX indicator. ET_DYN can be a shared
|
||||
library, and an absent canary symbol does not prove every function unprotected.
|
||||
|
||||
When complete layout Evidence is already retained, `inspect_analysis_view` /
|
||||
`inspect-analysis-view` projects a summary, the mitigations or linkage facet,
|
||||
one section or symbol, or a stable page without re-running the decoder. CLI
|
||||
one-shot invocations pass portable inline Evidence JSON; an MCP session uses
|
||||
the retained `evidence_id`. Page `limit` is required and bounded by the
|
||||
measured MCP stdio budget documented on the tool contract.
|
||||
|
||||
Complete results have a 32 MiB input, 64 MiB reply, 1 MiB combined diagnostics
|
||||
and 30-second owned command deadline. The reply budget applies to the decoder
|
||||
record; the CLI/MCP Evidence envelope adds copies and encoding overhead. The
|
||||
|
||||
@@ -109,6 +109,13 @@ cached result. `close_binary` accepts `snapshot_path` and optional
|
||||
`overwrite: true` to save before releasing provider resources. A failed save
|
||||
leaves the session open so the caller can resolve the output failure.
|
||||
|
||||
The MCP save receipt reports `primitive_entries`, `workflow_entries`, and
|
||||
`evidence_records` separately. Zero primitive bindings can still accompany retained workflow
|
||||
results and Evidence. These are cached observations, not a saved provider
|
||||
database: only eligible exact queries can reuse a result, and new or live
|
||||
queries can still require provider startup. The CLI uses the same snapshot
|
||||
format and preserves these records when loading and updating it.
|
||||
|
||||
## Import, export and compare Evidence
|
||||
|
||||
Evidence records retain artifact identity, source locations, observations,
|
||||
|
||||
+86
-9
@@ -195,8 +195,9 @@ Before applying changes, REA checks your current configuration. The plan lists:
|
||||
|
||||
Malformed or unsafe existing configuration blocks the whole transaction before
|
||||
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.
|
||||
changes. Agent configuration writes preserve unrelated entries and comments,
|
||||
create backups, use atomic replacement, and verify their result. Setup and
|
||||
uninstall retain an existing `.rea.backup` rather than replacing the first snapshot.
|
||||
|
||||
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
|
||||
@@ -310,6 +311,19 @@ host mount and the rest of `/tmp` remain unchanged; this fallback never invokes
|
||||
`sudo`. `rea doctor --provider hopper --json` reports the selected
|
||||
strategy and both host and effective mount facts.
|
||||
|
||||
The supported Linux demo forwards additional launches into its existing
|
||||
application, even across private displays. REA therefore reserves one Linux
|
||||
Hopper application per user across cooperating REA processes. A competing
|
||||
CLI or MCP session receives the owning session ID before Hopper is launched,
|
||||
for both the same target and a different target. Switch targets through that
|
||||
MCP session, or close it before starting another session. Closing the owner
|
||||
releases the reservation. This does not attach to or claim ownership of
|
||||
manually opened Hopper applications.
|
||||
|
||||
Stale lease recovery is serialized. If an interrupted recovery leaves its
|
||||
reservation directory, the diagnostic identifies that exact path. Stop REA
|
||||
sessions before removing that recovery directory and retrying.
|
||||
|
||||
### Launcher paths and troubleshooting
|
||||
|
||||
On macOS, REA uses
|
||||
@@ -479,14 +493,14 @@ 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
|
||||
Ghidra's default analysis settings, and loads its packaged Java
|
||||
bridge via `-scriptPath`; it never opens an existing user project. Linux and
|
||||
macOS use a current-user-only local bridge socket and descriptor. The
|
||||
project remains under the selected temporary directory. If its Unix socket
|
||||
pathname would exceed the host's byte limit, REA allocates a separate mode-0700
|
||||
socket directory under `/tmp` and removes it on close, cancellation, or failure.
|
||||
Diagnostics retain the actual endpoint and both owned directories.
|
||||
On macOS, REA starts the inspected JVM directly using Ghidra's own LaunchSupport
|
||||
On Linux and macOS, REA starts the inspected JVM directly using Ghidra's own LaunchSupport
|
||||
configuration. Apple platform shell wrappers hide their environments from
|
||||
ownership inspection, so retaining those wrappers would prevent verified
|
||||
process-group cancellation during startup.
|
||||
@@ -501,8 +515,9 @@ import and auto-analysis. The first Ghidra-backed query starts that work lazily.
|
||||
The provider startup deadline is 330,000 ms by default for import, analysis,
|
||||
bridge, and health readiness. Large binaries can need more: set
|
||||
`REA_GHIDRA_STARTUP_TIMEOUT_MS` to an integer between 1 and 2,147,483,647
|
||||
milliseconds in the server environment. An absent, empty, invalid or out-of-range
|
||||
value keeps the default. REA parses the supplied configuration environment and
|
||||
milliseconds in the server environment. An absent setting uses the default;
|
||||
empty, invalid or out-of-range supplied values fail configuration validation.
|
||||
REA parses the supplied configuration environment and
|
||||
passes the deadline to each provider client; a startup failure is returned by the query that triggered it,
|
||||
without exposing partial analysis. This deadline is separate from MCP transport
|
||||
initialization and the client's deadline for that individual tool call. A client
|
||||
@@ -519,6 +534,65 @@ is no fixed per-operation or response-size ceiling. Unresolved computed calls
|
||||
remain unknown, reference-kind provenance is preserved, and provider-specific
|
||||
pseudocode is never treated as original source or Hopper-equivalent text.
|
||||
|
||||
### Ghidra heap and CPU controls
|
||||
|
||||
Set resource controls in the environment that launches the CLI or MCP server,
|
||||
before opening a new Ghidra session. On Linux and macOS, REA chooses the first
|
||||
nonempty setting in this order: `GHIDRA_HEADLESS_MAXMEM`, `GHIDRA_MAXMEM`, then
|
||||
`2G`. The chosen value becomes the JVM's `-Xmx` argument. Use a JVM heap-size
|
||||
value such as `512M` or `2G`; an invalid value causes Java startup to fail.
|
||||
The supported Ghidra 12.1.4 Windows headless script consumes the same settings
|
||||
with the same precedence.
|
||||
|
||||
For a small target on a constrained host:
|
||||
|
||||
```bash
|
||||
export GHIDRA_HEADLESS_MAXMEM=512M
|
||||
```
|
||||
|
||||
In PowerShell:
|
||||
|
||||
```powershell
|
||||
$env:GHIDRA_HEADLESS_MAXMEM = "512M"
|
||||
```
|
||||
|
||||
An MCP client must pass this setting to its REA server process; changing an
|
||||
unrelated terminal's environment does not reconfigure an existing server or
|
||||
JVM. A 512 MiB heap was verified with small native fixtures, but larger targets
|
||||
can require more memory. The Java heap limit does not bound JVM RSS, Node's
|
||||
heap, or total memory used by the process family. The startup deadline controls
|
||||
waiting time independently of these memory settings.
|
||||
|
||||
REA clears `_JAVA_OPTIONS`, `JAVA_TOOL_OPTIONS`, `JDK_JAVA_OPTIONS`, and
|
||||
`GHIDRA_JAVA_OPTIONS` inherited from the caller, and owns
|
||||
`GHIDRA_HEADLESS_JAVA_OPTIONS`. Custom flags injected through these variables
|
||||
are not supported configuration. On Windows, REA replaces `JDK_JAVA_OPTIONS`
|
||||
with its own isolated home/temp settings and `-XX:-UsePerfData`; on Linux and
|
||||
macOS it supplies isolated paths directly to Java. Use the supported heap
|
||||
settings above instead of adding a second `-Xmx` through an injection variable.
|
||||
|
||||
The Linux/macOS launch sets `-XX:ParallelGCThreads=2` and
|
||||
`-XX:CICompilerCount=2`; the supported Windows headless script also sets these
|
||||
counts. These constrain particular JVM thread pools, not all analysis threads
|
||||
or CPU usage. REA does not pass Ghidra's `-max-cpu` option or expose a general
|
||||
Ghidra CPU limit. When needed, apply host CPU affinity and scheduling controls
|
||||
to the REA process and verify the resulting JVM's affinity. Select CPUs allowed
|
||||
on the host; do not assume CPU numbers from another machine are valid.
|
||||
|
||||
Verify controls at their consumer after the first Ghidra-backed query starts
|
||||
the JVM: inspect that owned JVM's effective `-Xmx` and GC/compiler flags, CPU
|
||||
affinity, and observed memory use. Do not infer them from the parent shell's
|
||||
environment or the absence of an error. On Linux, `/proc/<jvm-pid>/cmdline`
|
||||
contains the launch arguments and `/proc/<jvm-pid>/status` reports
|
||||
`Cpus_allowed_list` and `VmRSS`. Inspect only the process belonging to this
|
||||
session and retain the resource flags needed for the measurement rather than
|
||||
copying full command lines or environments into reports. Heap and affinity
|
||||
settings affect cost and latency; they are not part of REA's semantic analysis
|
||||
profile identity. See [real-provider testing](testing.md) for bounded
|
||||
verification workflows.
|
||||
|
||||
### Ghidra diagnostics and verification
|
||||
|
||||
Unexpected request failures retain their original internal cause. CLI and MCP
|
||||
errors expose Error names, messages, and available codes under
|
||||
`details.diagnostics.failure_cause`; other rejection values retain their
|
||||
@@ -560,8 +634,11 @@ rea uninstall --purge-data
|
||||
|
||||
Uninstall preserves Hopper, Node.js, Evidence files, captures, unrelated skills
|
||||
and other MCP servers. Purging removes only REA's cache and state under
|
||||
`~/.rea`; malformed client configuration or purge-path symlinks stop the
|
||||
operation. See the [CLI guide](cli.md#output-and-exit-status) for exit statuses.
|
||||
`~/.rea`. A client configuration that is malformed, unreadable, or at an unsafe
|
||||
path stops the operation before anything is removed, and a client that fails
|
||||
while being updated stops the remaining removals. A purge path that is a
|
||||
symbolic link is retained and reported rather than followed. See the
|
||||
[CLI guide](cli.md#output-and-exit-status) for exit statuses.
|
||||
|
||||
## MCP Registry
|
||||
|
||||
@@ -582,7 +659,7 @@ For a client that requires manual configuration, use:
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@6.1.0", "mcp"]
|
||||
"args": ["-y", "rea-agents@6.2.0", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
|
||||
REA derives a complete reachable feature trace from one authenticated JavaScript
|
||||
Application Graph and compares two authenticated graph versions. The MCP tools
|
||||
are `trace_application_feature`, `trace_javascript_semantics`,
|
||||
`compare_application_versions`, `compare_source_to_bundle`, and
|
||||
`compare_javascript_export_shapes`; their CLI equivalents use the same names
|
||||
with hyphens.
|
||||
are `inspect_analysis_view`, `trace_application_feature`,
|
||||
`trace_javascript_semantics`, `compare_application_versions`,
|
||||
`compare_source_to_bundle`, and `compare_javascript_export_shapes`; their CLI
|
||||
equivalents use the same names with hyphens. `inspect_analysis_view` projects a
|
||||
summary, module page, or one module from already completed application
|
||||
Evidence; it does not walk relationships.
|
||||
|
||||
These workflows consume Evidence produced by
|
||||
`inspect_analysis_view` also projects retained `inspect_binary_layout`
|
||||
Evidence. The remaining 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
|
||||
@@ -138,14 +141,33 @@ Nested callable returns are not assigned to their parent callable. Projected
|
||||
graph observations carry source ranges but never source text.
|
||||
|
||||
Return variants pair only when a literal discriminant such as `/type` has one
|
||||
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, 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.
|
||||
unique occurrence on each side and pairing is reciprocal. Source order never
|
||||
implies correspondence. Each retained variant contributes a property inventory
|
||||
of observed JSON Pointer names, including fields whose static values stay
|
||||
unknown, plus that variant's parent-property coverage.
|
||||
|
||||
Changes use JSON Pointer paths with `added`, `removed`, `changed`, or `unknown`
|
||||
status and a separate `presence` object (`present`, `absent`, or
|
||||
`unknown-coverage`) on each side. A name is `added` or `removed` when one paired
|
||||
shape proves presence and the other proves absence. Complete parent-property
|
||||
coverage can establish absence even when field values stay unresolved. Identical literals are
|
||||
omitted. Unresolved values that remain on both sides stay `unknown` when the
|
||||
projections differ, and are omitted when presence is unchanged and the unknown
|
||||
projections match. Incomplete spreads and other partial parent coverage keep
|
||||
one-sided names `unknown` with `unknown-coverage` on the incomplete side.
|
||||
Unpaired variants remain visible as unknown changes and still list their
|
||||
property inventories, each with its own `source_range`. Inventories exclude
|
||||
array holes and slots whose presence was invalidated by mutation; an unresolved
|
||||
value alone does not make an observed property uncertain. `summary.added` and
|
||||
`summary.removed` count
|
||||
presence-level add/remove as well as literal value add/remove;
|
||||
`summary.unknown` does not absorb complete-coverage presence-only gaps.
|
||||
|
||||
The output includes exact selector candidates, 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
|
||||
|
||||
@@ -167,8 +189,8 @@ writeFileSync("trace-input.json", JSON.stringify({
|
||||
rea trace-application-feature ./trace-input.json --json
|
||||
```
|
||||
|
||||
MCP analysis returns an envelope containing `result`, `evidence_id`, and full
|
||||
`evidence`. If the connected server advertises retained references, reuse its
|
||||
MCP analysis returns the complete Evidence record, with the operation result
|
||||
in `normalized_result` and its identity in `evidence_id`. If the connected server advertises retained references, reuse its
|
||||
exact returned ID as `{"kind":"retained-evidence","evidence_id":"RETURNED_ID"}`
|
||||
in `application`, or `left`/`right` for comparisons. `RETURNED_ID` is a template,
|
||||
not a literal valid ID. Native Evidence arrays still use complete records.
|
||||
|
||||
@@ -108,14 +108,15 @@ memory needed to construct the analysis graph itself.
|
||||
MCP prepares the complete repeated response incrementally against the pinned
|
||||
SDK's 10 MiB stdio receive-buffer budget. Oversized results return an actionable
|
||||
transport constraint and the exact same-session Evidence reference. Use
|
||||
`trace_application_feature` to inspect a selected module's relationships, or
|
||||
`export_evidence_bundle` to write the complete canonical bundle without a
|
||||
document-sized allocation. Same-session analysis reads reuse authenticated
|
||||
immutable snapshots; foreign inline Evidence is still parsed and authenticated.
|
||||
`inspect_analysis_view` with the retained Evidence ID for a summary, one
|
||||
module, or a stable page of module identities. Use `trace_application_feature`
|
||||
once a module, route, or string seed is known, or `export_evidence_bundle` to
|
||||
write the complete canonical bundle without a document-sized allocation.
|
||||
Same-session analysis reads reuse authenticated immutable snapshots; foreign
|
||||
inline Evidence is still parsed and authenticated.
|
||||
See [MCP tool results](mcp-contracts.md#tool-results) for larger client buffers
|
||||
and `REA_MCP_MAX_RESPONSE_BYTES`. Follow-up results remain complete and can also
|
||||
exceed the transport budget. Compact result views are tracked separately in
|
||||
[#1050](https://github.com/morluto/rea/issues/1050).
|
||||
exceed the transport budget.
|
||||
|
||||
## What is reconstructed
|
||||
|
||||
@@ -293,8 +294,11 @@ do not select a later entry. Rejected references retain the package metadata
|
||||
path, original target, failed constraint, and importing source location.
|
||||
|
||||
HTML script references resolve to exact inventoried files after applying the
|
||||
document base and query/fragment rules. CommonJS module lookups retain extension
|
||||
and directory resolution.
|
||||
document base and query/fragment rules. As in a browser, the script URL and base
|
||||
href are read without surrounding whitespace or embedded tabs and newlines, and
|
||||
their percent-encoded path bytes are decoded; encoded dot and separator bytes
|
||||
are still rejected. CommonJS module lookups retain extension and directory
|
||||
resolution.
|
||||
Unresolved HTML references retain their declaration, source range, and resolution
|
||||
reason in the renderer observations.
|
||||
HTML script source ranges follow the HTML parser across LF, CRLF, and bare CR
|
||||
|
||||
+71
-32
@@ -2,7 +2,7 @@
|
||||
|
||||
## Generated catalog
|
||||
|
||||
Run `npm run build:cached` in a source checkout to generate the machine-readable
|
||||
Run `npm run docs:generate` in a source checkout to generate the machine-readable
|
||||
catalog at `docs/public/product-catalog.json`. Documentation deployments serve
|
||||
the same file at [/rea/product-catalog.json](/rea/product-catalog.json). PR CI retains it with the packaged
|
||||
skill and portable conformance projections in the `generated-docs` artifact.
|
||||
@@ -24,22 +24,28 @@ health transition leaves that catalog unchanged and does not emit
|
||||
`notifications/tools/list_changed`.
|
||||
|
||||
Advertised input and output schemas contain no reachable recursive references.
|
||||
The input compatibility profile limits nesting to ten object, array, and
|
||||
`anyOf`/`oneOf`/`allOf` levels, following local references per path. Property
|
||||
maps, reference definitions, and example data do not add schema levels. Tests
|
||||
check the complete catalog after SDK conversion and its generated counterpart.
|
||||
This is REA's local compatibility profile; individual model APIs can impose
|
||||
additional limits.
|
||||
Schemas share repeated definitions through schema-local references while
|
||||
preserving complete fields and validation rules. Input properties retain their
|
||||
descriptions and literal examples. The SDK advertises an object root for
|
||||
canonical object unions and retains each branch's requirements and exclusions.
|
||||
Tests validate the schemas after SDK conversion and compare advertised
|
||||
validation with actual calls. Individual model APIs can impose additional
|
||||
nesting limits; complete producer captures can exceed ten structural levels.
|
||||
|
||||
For passive `compare_web_captures` inputs, pass each complete
|
||||
`inspect_web_page` result in `before.inspection` or `after.inspection`, with an
|
||||
optional complete `discover_webmcp_tools` result in the matching `webmcp` field.
|
||||
The input schema describes these producer-result objects as round-trip payloads.
|
||||
For scenario comparisons, pass complete `capture_browser_scenario` results in
|
||||
`before_scenario` and `after_scenario`. REA validates all nested fields with the
|
||||
original capture schemas; observation output schemas remain complete. Scenario
|
||||
authoring inputs and comparison normalization options retain their full
|
||||
advertised structure.
|
||||
`compare_web_captures` accepts exactly one of two input shapes:
|
||||
|
||||
- Passive: `before` and `after` each contain `inspection`, the complete
|
||||
`normalized_result` from `inspect_web_page`. Each may also contain `webmcp`,
|
||||
the complete `normalized_result` from `discover_webmcp_tools`, or null.
|
||||
- Scenario: `before_scenario` and `after_scenario` contain complete
|
||||
`normalized_result` objects from `capture_browser_scenario`. Optional
|
||||
`normalization` defaults to `{ "rules": [] }`.
|
||||
|
||||
The advertised schema includes complete nested capture fields and rejects
|
||||
incomplete pairs, mixed comparison families, and structurally malformed
|
||||
captures. Domain validation additionally checks relationships such as event
|
||||
sequence references and retained counts; JSON Schema does not express those
|
||||
cross-field invariants.
|
||||
|
||||
Call `binary_session` with `{}` and read `result.tool_availability` to choose a
|
||||
callable operation for the current target, provider, host, and negotiated client
|
||||
@@ -71,6 +77,15 @@ inventories, and do not claim that no short-lived descendant existed.
|
||||
|
||||
## Progress and cancellation
|
||||
|
||||
Tool execution failures return `isError: true` and the complete canonical
|
||||
`{ "error": ... }` diagnostic as JSON in text `content`. Read that diagnostic
|
||||
for the error code, target details, partial observations, and remediation.
|
||||
Failures omit `structuredContent`, since each advertised `outputSchema`
|
||||
describes successful results. Successful replies retain their schema-validated
|
||||
structured data and matching text projection. Oversized errors are retained
|
||||
as Evidence before delivery of a bounded diagnostic with an export reference;
|
||||
if retention is unavailable or fails, the diagnostic reports that reason.
|
||||
|
||||
REA accepts ordinary `tools/call` progress tokens. Updates are monotonic,
|
||||
rate-bounded to at most one intermediate update per 100 ms, and always allow a
|
||||
terminal update. Unknown totals are omitted; REA does not fabricate percentages.
|
||||
@@ -97,9 +112,11 @@ still run synchronously, so control messages can wait for those
|
||||
phases to release the event loop. A rejected client promise alone does not
|
||||
establish that the server has stopped its work.
|
||||
|
||||
CLI calls work without a progress token and translate SIGINT into the same
|
||||
AbortSignal used by providers. Existing controlled-process cleanup and provider
|
||||
shutdown rules still apply; REA never kills a process it cannot prove it owns.
|
||||
Direct CLI analysis calls work without a progress token and translate SIGINT or
|
||||
SIGTERM into the same AbortSignal used by providers. Signal guards remain active
|
||||
through provider cleanup, including repeated delivery by package runners.
|
||||
Existing controlled-process cleanup and provider shutdown rules still apply;
|
||||
REA never kills a process it cannot prove it owns.
|
||||
|
||||
## Ghidra first-query deadlines and recovery
|
||||
|
||||
@@ -111,11 +128,11 @@ auto-analysis, bridge connection, and health readiness before returning analysis
|
||||
|
||||
These deadlines have different owners:
|
||||
|
||||
| Deadline | Owner and effect |
|
||||
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| MCP initialize | The client bounds transport/REA connection startup, before any target query. |
|
||||
| Ghidra startup | REA allows 330,000 ms by default; `REA_GHIDRA_STARTUP_TIMEOUT_MS` accepts 1–2,147,483,647 ms. Invalid values keep the default. Startup failure is reported by the first provider query. |
|
||||
| Individual tool call | The client bounds its wait, including cold engine startup. The pinned client SDK 2.3.1 defaults to 60,000 ms and can cancel earlier than REA's startup deadline. |
|
||||
| Deadline | Owner and effect |
|
||||
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| MCP initialize | The client bounds transport/REA connection startup, before any target query. |
|
||||
| Ghidra startup | REA allows 330,000 ms by default; `REA_GHIDRA_STARTUP_TIMEOUT_MS` accepts decimal integers from 1–2,147,483,647 ms. Invalid supplied values fail configuration validation. Startup failure is reported by the first provider query. |
|
||||
| Individual tool call | The client bounds its wait, including cold engine startup. The pinned client SDK 2.3.1 defaults to 60,000 ms and can cancel earlier than REA's startup deadline. |
|
||||
|
||||
For an already connected client using the pinned SDK, request options are the
|
||||
**second** argument of `callTool`:
|
||||
@@ -171,26 +188,36 @@ increasing a client deadline alone does not fix those failures.
|
||||
|
||||
## Tool results
|
||||
|
||||
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
|
||||
Custom clients upgrading from 6.1 should follow the
|
||||
[6.2 migration guide](migration-6.2.md#mcp-results-and-evidence).
|
||||
|
||||
Evidence-producing tools return the complete canonical Evidence record in both
|
||||
text and structured content. Read `structuredContent.normalized_result` for the
|
||||
operation result and `structuredContent.evidence_id` for its identity. The same
|
||||
record is retained in the session bundle. `analysis_profile` is always present:
|
||||
a concrete profile object or `null`; either value participates in semantic
|
||||
identity. Records omitting this field are rejected. Pass the returned Evidence
|
||||
directly 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.
|
||||
|
||||
REA prepares complete MCP results within the pinned stdio client's 10 MiB
|
||||
receive-buffer budget, including both text and structured representations and
|
||||
room for the JSON-RPC envelope. If a result cannot fit, REA returns
|
||||
room for the JSON-RPC envelope. Tool errors carry only their text
|
||||
representation, so only that text counts against the budget. Response budget settings are captured at startup;
|
||||
restart or recreate the server to apply changes. If a result cannot fit, REA returns
|
||||
`resource_constraint` with `details.resource: "transport"` before constructing
|
||||
a document-sized string. Analysis Evidence remains complete in the current
|
||||
session. Its exact reference is reported in
|
||||
`details.reported_limits.evidence_reference`; use it with a focused application
|
||||
workflow, or call `export_evidence_bundle` with a destination path. Complete
|
||||
`details.reported_limits.evidence_reference`; call `inspect_analysis_view` with
|
||||
that reference for a summary, one section/module, or a stable page, or call
|
||||
`export_evidence_bundle` with a destination path. Complete
|
||||
bundle exports stream canonical JSON into an atomically published file. A broad
|
||||
follow-up or `get_evidence_bundle` can also exceed the response budget; exporting
|
||||
preserves the complete session without sending it through a single MCP frame.
|
||||
Cancelling an export stops further serialization and removes its staging file.
|
||||
The destination changes only when a complete export is atomically published.
|
||||
|
||||
Clients that explicitly configure a larger receive buffer can set the REA
|
||||
server's `REA_MCP_MAX_RESPONSE_BYTES` environment variable to the same byte
|
||||
@@ -202,6 +229,18 @@ Ordinary responses keep their existing complete result contract.
|
||||
|
||||
## Retained application Evidence inputs
|
||||
|
||||
`inspect_analysis_view` projects a caller-selected view of already completed
|
||||
`inspect_binary_layout` or `analyze_javascript_application` Evidence. Source is
|
||||
an exact same-session retained reference or portable inline Evidence. Views are
|
||||
a summary, a layout mitigations or linkage facet, one section/symbol/module, or
|
||||
a stable page with a caller-selected positive `limit`. Module pages include
|
||||
JavaScript assets, bundled modules, and source modules. Select an exact
|
||||
`node_id` when a path is ambiguous or unavailable. Module items retain their
|
||||
recorded property values and source locations; summaries include parent
|
||||
application and semantic coverage. The result carries a digest of the selected
|
||||
facts. Actual serialized size determines MCP transport admission: reduce the
|
||||
page size or export retained Evidence if the view is too large.
|
||||
|
||||
`trace_application_feature`, `trace_javascript_semantics`,
|
||||
`compare_application_versions`, `compare_source_to_bundle`, and
|
||||
`compare_javascript_export_shapes` accept complete inline application Evidence
|
||||
|
||||
+3
-1
@@ -27,7 +27,9 @@ prompt argument or tool explicitly accepts one.
|
||||
|
||||
For a JavaScript/Electron application directory, `investigate_feature` points
|
||||
directly to `analyze_javascript_application` with `input_path`; it does not
|
||||
require `open_binary`. The CLI equivalent is `rea analyze <directory>`.
|
||||
require `open_binary`. When that Evidence, or `inspect_binary_layout` Evidence,
|
||||
is already retained, it suggests `inspect_analysis_view` before tracing a
|
||||
feature seed. The CLI equivalent is `rea analyze <directory>`.
|
||||
`open_binary` and the doctor target check admit files and macOS app bundles.
|
||||
A directory outside that opening route is reported with an available analysis
|
||||
action; this does not establish support for every directory format.
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
# Upgrading from REA 6.1 to 6.2
|
||||
|
||||
REA 6.2 includes stability fixes and changes to public result and saved-data
|
||||
contracts. The repository uses `always-bump-minor`, including breaking releases;
|
||||
the minor version increment does not imply backward compatibility. Refresh cached
|
||||
MCP tool definitions after upgrading and adapt custom consumers using this guide.
|
||||
|
||||
## MCP results and Evidence
|
||||
|
||||
Evidence-producing tools now return the complete Evidence record directly in
|
||||
`structuredContent` and JSON text. The old `{ result, evidence_id, evidence }`
|
||||
wrapper is gone.
|
||||
For a successful Evidence-producing call, change:
|
||||
|
||||
```javascript
|
||||
// 6.1
|
||||
const value = reply.structuredContent.result;
|
||||
const evidence = reply.structuredContent.evidence;
|
||||
|
||||
// 6.2
|
||||
const evidence = reply.structuredContent;
|
||||
const value = evidence.normalized_result;
|
||||
const evidenceId = evidence.evidence_id;
|
||||
```
|
||||
|
||||
Check `isError` before reading success fields. Error results have `isError: true`
|
||||
and carry their typed JSON diagnostic in a text content block; they omit
|
||||
`structuredContent` so they cannot violate the advertised success output schema.
|
||||
|
||||
```javascript
|
||||
if (reply.isError === true) {
|
||||
const text = reply.content.find((part) => part.type === "text");
|
||||
if (text === undefined) throw new Error("REA error has no text diagnostic");
|
||||
const diagnostic = JSON.parse(text.text);
|
||||
// Handle diagnostic.code, diagnostic.message and diagnostic.details.
|
||||
}
|
||||
```
|
||||
|
||||
Lifecycle and other non-Evidence tools still use their own advertised output
|
||||
contracts. Do not assume every successful tool returns Evidence. Consult
|
||||
[`tools/list` and the result guide](mcp-contracts.md#tool-results).
|
||||
|
||||
A transport `resource_constraint` error can include a retained
|
||||
`details.reported_limits.evidence_reference`. Use `inspect_analysis_view` or
|
||||
`export_evidence_bundle` on the same connection to consume the complete retained
|
||||
Evidence. Response-budget settings are captured when the server starts; recreate
|
||||
the server to change them, and keep the client's receive budget aligned. Raising
|
||||
the server budget does not change the client's limit or analysis coverage.
|
||||
|
||||
Every Evidence record now requires `analysis_profile`, either its observed
|
||||
profile object or `null`. The field participates in Evidence identity. Records
|
||||
that omitted it cannot be migrated by inserting `null` while retaining the old
|
||||
`evidence_id`. Keep the original files and regenerate affected observations with
|
||||
6.2; use the new returned identities when linking or comparing them. Existing
|
||||
records that already satisfy the complete canonical contract remain usable.
|
||||
|
||||
The `binary_session` result removes the redundant top-level `provider` and
|
||||
`providers` aliases. Read `analysis_provider_binding`,
|
||||
`analysis_provider_candidates` and per-operation `capabilities` for selection
|
||||
and availability. Select a deep provider through `open_binary.provider_id`
|
||||
or CLI `--provider`, using the advertised values.
|
||||
|
||||
These changes establish one complete result representation and explicit
|
||||
provider/profile ownership rather than accepting incomplete producer records.
|
||||
See [63bf914](https://github.com/morluto/rea/commit/63bf91405cf97b114a84f337fb1272d98a267999)
|
||||
and [#1277](https://github.com/morluto/rea/pull/1277).
|
||||
|
||||
## Saved snapshots and artifact inventories
|
||||
|
||||
Analysis snapshots require `workflow_entries` and a corresponding profile-bound
|
||||
Evidence record for every cached query. Import rejects incomplete bindings
|
||||
instead of silently discarding them. Preserve an incompatible snapshot and
|
||||
choose a new output path when rerunning analysis, for example:
|
||||
|
||||
```bash
|
||||
rea analyze /absolute/path/to/program --provider ghidra --snapshot /absolute/path/to/analysis/program-6.2.json
|
||||
```
|
||||
|
||||
In MCP, open the target without the incompatible `snapshot_path`, run the
|
||||
required queries, then save to a new path through `close_binary.snapshot_path`.
|
||||
Do not fabricate missing provider profiles or query bindings. See
|
||||
[snapshot usage](cli.md#save-and-reuse-analysis-snapshots).
|
||||
|
||||
Artifact inventory nodes identify content bytes. Path-specific role and
|
||||
permissions now belong to each occurrence:
|
||||
|
||||
| Previously read | Read in 6.2 |
|
||||
| -------------------------------- | ----------------------------------------- |
|
||||
| `nodes[].kind` | `occurrences[].artifact_kind` |
|
||||
| Path-specific `nodes[].format` | `occurrences[].artifact_format` |
|
||||
| `nodes[].executable` | `occurrences[].executable` |
|
||||
| Content identity and byte format | `nodes[].artifact_id`, `sha256`, `format` |
|
||||
|
||||
Join an occurrence to its content node using `artifact_id`; inspect the selected
|
||||
occurrence's role rather than choosing one role for all paths with equal bytes.
|
||||
An executable binary format can have `artifact_kind: "executable"` while its
|
||||
current filesystem permission produces `executable: false`. A `.js` and `.txt`
|
||||
file with identical bytes can share a node and retain different roles.
|
||||
Regenerate old inventories: a merged content node alone cannot recover the
|
||||
per-path facts that were lost. These changes are part of
|
||||
[63bf914](https://github.com/morluto/rea/commit/63bf91405cf97b114a84f337fb1272d98a267999).
|
||||
|
||||
## Process captures and cleanup failures
|
||||
|
||||
REA no longer converts legacy v3 process captures on import. Captures require
|
||||
explicit selected/launch executable identity and `event_journal`; missing data
|
||||
is rejected instead of being filled during parsing. Preserve old captures for
|
||||
historical reference and recapture with 6.2 when current comparisons are needed.
|
||||
Do not infer the original launched binary from a later file digest, or invent
|
||||
event ordering from independently recorded collections. An explicitly empty
|
||||
journal is valid under its advertised contract; the field cannot be omitted.
|
||||
See [2a77469](https://github.com/morluto/rea/commit/2a7746998d47446c4db6d4f524e0f183d6803f3f).
|
||||
|
||||
Filesystem effects can now report `status: "unknown"` with a reason when a
|
||||
partial snapshot cannot establish creation or deletion. Update exhaustive
|
||||
status readers and retain observed facts separately from absence assumptions.
|
||||
|
||||
A completed one-shot analysis can report `cleanup_incomplete` rather than
|
||||
silently returning success when provider cleanup fails. Preserve its
|
||||
`details.partial_observation` or primary execution diagnostics, inspect the
|
||||
reported owned resources, and address cleanup before retrying. A successful
|
||||
analysis observation does not prove successful cleanup. This behavior is covered
|
||||
by [#1211](https://github.com/morluto/rea/pull/1211). That change also makes
|
||||
configured agent-evaluation fixture checks grade cited Evidence; routing or
|
||||
answer wording alone no longer passes an assessed fixture.
|
||||
|
||||
## JavaScript and function comparisons
|
||||
|
||||
JavaScript return-field projections require `presence` with `present`, `absent`
|
||||
or `unknown-coverage`. Export-shape change rows retain both sides' presence,
|
||||
and `property_inventories` report examined properties and coverage. Unknown
|
||||
values and unknown presence are distinct; callers must not treat an unknown
|
||||
value as a missing property. Semantic object-operation graph observations use
|
||||
`properties.property_path`, an array of complete path segments or `null`,
|
||||
rather than a flattened property name. Regenerate old application analysis
|
||||
before depending on nested mutation or presence facts. See
|
||||
[475a0f8](https://github.com/morluto/rea/commit/475a0f84dac63d710e032bc7f731c1a3bda624ec).
|
||||
|
||||
Function comparison dimension statuses are `unchanged`, `changed` or `unknown`.
|
||||
The unreachable `truncated` status and `summary.truncated` counter have been
|
||||
removed. Update exhaustive enum handling and display logic; unavailable facts
|
||||
remain `unknown`. See
|
||||
[5510b27](https://github.com/morluto/rea/commit/5510b27d7f863a3df26212fba4a59f8df0da3712).
|
||||
|
||||
## Historical-source symlinks
|
||||
|
||||
Source graph symlinks with `target_state: "unreadable"` or `"unknown"` require
|
||||
`target: null`. Internal, external and missing targets retain their actual
|
||||
recorded target strings. External targets use the observed absolute target;
|
||||
display placeholders such as `<outside-root>` cannot serve as lookup data.
|
||||
Reimport the source tree when the old graph retained only a placeholder, and
|
||||
handle nullable targets in custom readers. See
|
||||
[b7b4333](https://github.com/morluto/rea/commit/b7b433370310c3ffe33d5027d8393151d5e862da).
|
||||
|
||||
Keep original saved artifacts until their replacements and consuming workflows
|
||||
have been checked. Fresh analysis may establish different facts if the target,
|
||||
source tree or runtime changed; it does not rewrite what an older capture proved.
|
||||
@@ -128,6 +128,9 @@ an explicit `0x` or address-space prefix selects an address; otherwise an exact
|
||||
database symbol name takes precedence over a bare hexadecimal address. Thus a
|
||||
function renamed to `dead` remains selectable by name. Use the names returned
|
||||
by the inventory, including their namespaces and any platform symbol prefix.
|
||||
Ghidra also accepts secondary symbols at a function entry, such as an imported
|
||||
`_main` label. A label inside a function is not a procedure-name selector; use
|
||||
its address when selecting the containing function.
|
||||
Overloads can share a fully qualified name; ambiguity errors return every
|
||||
matching entry address so the caller can select the intended function directly.
|
||||
|
||||
@@ -182,10 +185,35 @@ outside-memory and undecodable addresses have separate outcomes. Effective
|
||||
memory base/index/displacement roles and per-instruction context mode remain
|
||||
unavailable; `mode` is the program language variant.
|
||||
|
||||
Ghidra distinguishes a return using decoded instruction p-code's `RETURN`
|
||||
operation. `flow.classification_evidence` preserves the Listing flow type and
|
||||
the observed return evidence; mnemonic spelling alone is not the classifier.
|
||||
Other terminal or trap flows retain the provider's classification, and this
|
||||
inspection does not change function extents or prove a calling convention.
|
||||
The analysis profile commits this decoder behavior. Snapshots from the earlier
|
||||
profile produce a profile mismatch rather than replaying the former generic
|
||||
terminal result. The old snapshot's `evidence_bundle` remains valid retained
|
||||
Evidence: copy that object to a separate JSON file and use `rea evidence-import`
|
||||
to validate it. Run analysis without the old snapshot and save a fresh one
|
||||
under the current profile. Other providers and older captures may omit the
|
||||
classification evidence field.
|
||||
|
||||
Call resolution reports direct, resolved indirect, ambiguous, unresolved and
|
||||
non-call outcomes from static call references. It does not establish runtime
|
||||
execution or classify Objective-C/Swift/vtable/closure mechanisms from names.
|
||||
|
||||
`batch_decompile` keeps each caller-selected symbol or address in `items[].address`.
|
||||
The separate `procedure` field reports the canonical entry and the provider's
|
||||
observed entry label. An interior address therefore remains visible without
|
||||
being mistaken for the function entry. Identity failures are reported as
|
||||
`procedure.status: "unknown"`; label failures leave a null name and `name_error`.
|
||||
Neither failure discards usable pseudocode, and names are not parsed from it.
|
||||
Resolved entries also appear in the composed Evidence locations. New batch
|
||||
Evidence commits the revised workflow profile; retained historical Evidence
|
||||
remains valid. This tool
|
||||
has no dedicated CLI command; the existing `decompile` command remains the
|
||||
single-procedure CLI operation.
|
||||
|
||||
Type inspection selects one exact database pathname or typed data address.
|
||||
Struct/union fields, enums, pointers, arrays, typedefs, size, alignment, packing
|
||||
and bitfields retain database authority. Child types use exact IDs for further
|
||||
|
||||
+58
-1
@@ -94,7 +94,35 @@ 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.
|
||||
|
||||
Output, file count, file size, process sampling, filesystem depth, total
|
||||
A missing entry proves creation or deletion only when the corresponding
|
||||
observation root's path enumeration was exhausted. Otherwise its effect has
|
||||
`status: "unknown"`, the available before/after state, and a reason; a null
|
||||
state in this variant means unobserved. Paths below symlinks that REA did not
|
||||
follow also remain unknown. Partial enumeration of one root does not erase
|
||||
known effects in another, and unavailable content hashes do not by themselves
|
||||
make path absence unknown.
|
||||
|
||||
Terminal frame `data` is the comparison text after the selected normalization.
|
||||
New captures also preserve `raw_data` when normalization changes a PTY chunk;
|
||||
otherwise `data` is already the original text. Older captures may lack the
|
||||
original text for changed chunks. Both values participate in Evidence identity,
|
||||
while comparisons and trace assertions use the normalized text. Retaining the
|
||||
original adds at most `limits.output_bytes` of text, because it comes from the
|
||||
same admitted PTY chunks rather than a second unbounded stream.
|
||||
|
||||
Port normalization recognizes explicit `port`, `tcp_port`, `udp_port`, and
|
||||
`listen` fields, URL authorities, IP endpoints, and `localhost` endpoints, with
|
||||
ports from 0 through 65535. A port may end a sentence, as in `port: 8080.`;
|
||||
a period followed by a letter or digit continues a decimal, version, or host
|
||||
name instead. Each PTY chunk is normalized on its own, so a port whose period
|
||||
ends the chunk is left unchanged because its continuation is not yet known. It preserves ordinary counters, dimensions, file line numbers,
|
||||
and ambiguous bare host labels. Use explicit literal `patterns`
|
||||
for a different application-specific spelling. Set `ports: false` to preserve
|
||||
all endpoint numbers in comparison text. Captures commit the port-normalization
|
||||
version so older broad numeric normalization cannot silently compare as the
|
||||
same contract.
|
||||
|
||||
Output retention, file count, aggregate file-hashing bytes, 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
|
||||
@@ -102,6 +130,35 @@ 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.
|
||||
|
||||
New captures include `truncation_details`, with separate accounting for:
|
||||
|
||||
- `raw_terminal`: original UTF-8 PTY chunk bytes and observed/retained chunk
|
||||
counts. A chunk that does not fit `limits.output_bytes` is omitted whole;
|
||||
a later smaller chunk can still fit.
|
||||
- `rendered_terminal`: cumulative UTF-8 serialized-state and visible-line
|
||||
bytes, plus observed/retained frame counts. This is independent of the raw
|
||||
chunk budget and does not count JSON encoding. Repeated screen snapshots can
|
||||
exhaust it even when all raw output fits. Rendered states use retained PTY
|
||||
input, so raw omissions also limit rendered coverage.
|
||||
- `filesystem_before` and `filesystem_after`: file-count/depth limits,
|
||||
enumeration failures, whole-file hash budget and bytes successfully hashed.
|
||||
Each retained regular file without a digest has an aliased path, size,
|
||||
remaining budget and reason: `file_bytes_budget` or
|
||||
`file_changed_or_short_read`. A file too large for the remaining budget is
|
||||
skipped; a later smaller file can still be hashed. Hash omissions do not
|
||||
imply incomplete path enumeration.
|
||||
- `process`: sampling limit and whether sampling ended partially. Coverage
|
||||
remains `sampled`, including when that limit was not exhausted.
|
||||
|
||||
The legacy `truncated` flag summarizes these observations. Comparisons retain
|
||||
results for unaffected dimensions and mark affected dimensions unknown; an
|
||||
incomplete capture cannot locate the first divergence across all dimensions.
|
||||
Trace assertions use coverage for the sources they select, so an assertion
|
||||
about complete raw terminal output need not fail because rendered snapshots
|
||||
were omitted. Older truncated captures without these details retain the
|
||||
conservative comparison behavior. These diagnostics use the existing scenario
|
||||
budgets; they do not introduce a separate file-hashing budget.
|
||||
|
||||
When the host withholds an unrelated process’s ownership token, REA leaves that
|
||||
process untouched and records its PID and reason in `cleanup.unverified_processes`
|
||||
and process residual unknowns. Successful cleanup verifies the owned group;
|
||||
|
||||
@@ -35,6 +35,9 @@ registry metadata or changelog disagrees with that version before creating a tag
|
||||
|
||||
### Compatibility and version selection
|
||||
|
||||
For the 6.2 candidate, review the [6.1 to 6.2 migration guide](migration-6.2.md)
|
||||
alongside the changelog before publication.
|
||||
|
||||
REA uses Release Please's `always-bump-minor` strategy: every release increments
|
||||
minor and resets patch, including releases with breaking changes. For example,
|
||||
the next release after 6.0.0 is 6.1.0. Version numbers use the SemVer format,
|
||||
|
||||
+94
-10
@@ -1,13 +1,18 @@
|
||||
# Testing REA
|
||||
|
||||
Prefer evidence in this order: full end-to-end workflows with real production
|
||||
providers, integration tests across data/API boundaries, then golden regressions
|
||||
from real captured inputs. Keep focused module tests for distinct failure or
|
||||
semantic cases that these workflows cannot reliably reproduce. A test's path
|
||||
or suite name does not establish its behavioral depth.
|
||||
Prefer evidence in this order: full end-to-end workflows through the public
|
||||
CLI or MCP with real production providers and no mocked dependencies,
|
||||
integration across production data/API boundaries, then golden regressions
|
||||
from real captured inputs. Keep a focused module test only for a distinct
|
||||
failure or semantic case that stronger workflows cannot reliably reproduce.
|
||||
A test's path, compiled imports, or suite name does not establish its depth.
|
||||
|
||||
Avoid tests that mirror getters, enum mappings, fixture helpers, or prescribed
|
||||
call sequences, or assert cache object identity without a caller-visible contract.
|
||||
Delete tests that only mirror getters, serialization helpers, enum mappings,
|
||||
fixture helpers, prescribed call sequences, arbitrary counts or snapshots,
|
||||
or cache object identity without a caller-visible contract. Do not retain a
|
||||
weaker test merely because it is fast when a stronger workflow already proves
|
||||
the same claim. Keep format-specific malformed representations and actual
|
||||
capacity, cleanup, permission and target-identity regressions.
|
||||
Prefer representative workflows over a Cartesian matrix when rows cross the
|
||||
same boundary. Retain combinations that exercise a distinct interaction, such as
|
||||
filtering within a full-output envelope. Keep distinct command handlers,
|
||||
@@ -46,6 +51,15 @@ claim, even if a directory name calls it acceptance.
|
||||
Emit a verifier's final success report only after awaited cleanup. Observe final
|
||||
process lineage after closing its owned resources.
|
||||
|
||||
Owned JavaScript semantic graphs capture and schema-parse caller input before
|
||||
returning an iterator. Whole-graph hashing, batched integrity checks and sealing
|
||||
run through the application cancellation driver; only full completion grants
|
||||
the private immutable-graph proof. Regression coverage checks canonical IDs,
|
||||
validation diagnostics, nested caller mutations across yields, cancellation
|
||||
before result validation and subsequent SDK requests. Single-file parsing,
|
||||
schema cloning and canonical sorting remain synchronous phases; measure them
|
||||
separately when assessing cancellation latency on real applications.
|
||||
|
||||
The Vitest runner releases cached native process inspectors during awaited
|
||||
worker teardown. Fork termination does not run Node's normal exit hooks; a
|
||||
per-file teardown would instead retire shared inspectors before later files
|
||||
@@ -230,7 +244,12 @@ subsequent wire recovery and clean shutdown. Caller timeout does not interrupt
|
||||
Hopper's synchronous native operation. Socket boundary tests deterministically
|
||||
cover active and queued expiry, late replies and timer cleanup.
|
||||
|
||||
The Linux demo lane remains a separate `verify:hopper:linux` command.
|
||||
The Linux demo lane remains a separate `verify:hopper:linux` command. Its
|
||||
lifecycle checks attempt competing CLI launches for the active target and a
|
||||
different target, requiring rejection before either can change the owning
|
||||
MCP session's documents or procedures. Lease boundary tests also preserve
|
||||
unresponsive or malformed live endpoints while allowing confirmed stale
|
||||
sockets to recover.
|
||||
|
||||
`verify:hopper:fat` is a separate macOS lane requiring installed Hopper and the
|
||||
existing Xcode clang/lipo toolchain. It compiles arm64/x86-64 thin executables and
|
||||
@@ -308,11 +327,18 @@ prove behavior on the verified Ghidra 12.1.4 and JDK 21 build.
|
||||
| Ghidra lane | Supported runner/target | Additional local tools |
|
||||
| ------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| `npm run verify:ghidra` | Linux x64/arm64 ELF or macOS x64/arm64 Mach-O | Host C compiler, Ghidra 12.1.4, and full JDK 21 |
|
||||
| `npm run verify:ghidra:swift` | macOS x64/arm64 Mach-O | Host Swift 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; `-- --x86` selects native x86 PE | Ghidra 12.1.4, full JDK 21, and the matching native artifact |
|
||||
|
||||
The optional Swift lane compiles a source-owned fixture and checks modern and
|
||||
provider-demangled procedure names through real stdio MCP, with and without the
|
||||
dSYM companion. It checks unfiltered, structs, and literal-name filters,
|
||||
same-address alias evidence, deduplication, and explicit unresolved categories.
|
||||
It does not add a Swift prerequisite to the host C fixture lane.
|
||||
|
||||
The host lane also verifies namespaced C++ ABI symbols and annotation identity.
|
||||
That facet requires Ghidra's matching `demangler_gnu_v2_41` native component in
|
||||
`GPL/DemanglerGnu/os/<platform>/` or `build/os/<platform>/`, in addition to the
|
||||
@@ -437,6 +463,13 @@ Electron GUI behavior or another engine.
|
||||
|
||||
## Android APK analysis
|
||||
|
||||
The real Android lane also runs on Windows x64 with its matching bundled native
|
||||
controls. It verifies bidirectional protocol input, CLI/MCP result parity,
|
||||
real MCP cancellation/disconnect and Java exit after abrupt CLI owner
|
||||
termination. The forced Windows exit does not exercise the POSIX SIGTERM
|
||||
handler and can leave temporary workspace files. `verify:windows-native`
|
||||
separately checks binary input, backpressure, EOF and pending-write job closure.
|
||||
|
||||
`npm run verify:android` requires an existing Java 17+ and an explicit
|
||||
`REA_JADX_MCP_JAR` for jadx-headless-mcp 0.7.1. Set `REA_ANDROID_TEST_APK` to the
|
||||
fixed public ApiDemos v6.0.18 fixture. Obtain both with the explicit
|
||||
@@ -626,6 +659,17 @@ 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.
|
||||
|
||||
Source typechecking needs no compiled runtime or generated test metadata.
|
||||
`npm run build:cached` compiles the CLI/MCP runtime and its packaged skill.
|
||||
`npm run test:prepare` also generates the MCP contract test catalog, product
|
||||
catalog, and portable managed evidence used by the complete suite. Focused
|
||||
tests prepare those extra outputs only when their selected files consume them.
|
||||
The MCP test catalog is JSON in `.cache/mcp-tool-catalog.json`; the tracked
|
||||
test loader supplies its types without making generation a source-check
|
||||
prerequisite. Run `npm run mcp-catalog:generate` to refresh it independently.
|
||||
Documentation generation and managed conformance run through their separate
|
||||
`docs:generate` and `evidence:generate` task graphs.
|
||||
|
||||
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
|
||||
@@ -880,8 +924,48 @@ real local Codex CLI with:
|
||||
npm run verify:agent
|
||||
```
|
||||
|
||||
Its report records tool selection, repeated calls, token use, completion quality,
|
||||
and handling of permissions and unknowns.
|
||||
The packaged desktop and parser-comparison scenarios now assess a closed set of
|
||||
known fixture claims. Each final answer must be a strict JSON manifest containing
|
||||
exactly the requested claim IDs, values, Evidence IDs, and Evidence authority and
|
||||
confidence. Golden values are not included in the prompt. The evaluator checks
|
||||
exact values against both the fixture oracle and authenticated successful REA
|
||||
results; an Evidence ID by itself is insufficient. Missing, duplicate, extra,
|
||||
contradictory, incorrectly sourced, or unsupported claims fail the scenario.
|
||||
|
||||
The desktop rubric covers the exposed bridge API and members, renderer and main
|
||||
IPC operations, and the resolved preload. It binds its Evidence to the exact
|
||||
packaged artifact path and SHA-256. The parser rubric checks the precise heading
|
||||
depth addition, discriminant, complete comparison counts, and the explicit limit
|
||||
on runtime semantics. Its comparison must link to the two delivered source
|
||||
analyses and use the requested module and export selectors. Boundary tests package
|
||||
the same source-owned desktop fixture and compare the same source-owned parser
|
||||
files through actual MCP tool results, then verify that fabricated answers fail.
|
||||
|
||||
Per-scenario `factualCorrectness` is `passed`, `failed`, or `not_assessed` within
|
||||
the `configured_fixture_claims` scope. A pass establishes only the configured
|
||||
claims, not unrestricted factual correctness. Native, managed, browser,
|
||||
navigation-context, and address-context scenarios have no factual rubric and
|
||||
remain `not_assessed`; the managed workflow requires both artifact and member
|
||||
inspection to answer its type and entry-point question.
|
||||
|
||||
All scenarios retain routing, workflow, validation, repetition, process-exit,
|
||||
and token-use gates. Configured factual scenarios additionally require a factual
|
||||
pass. Their answer-text heuristics are diagnostic and do not affect the gate.
|
||||
Scenarios without a rubric retain the legacy text-heuristic gate.
|
||||
`answerTermCoverageMet` checks case-insensitive substrings,
|
||||
`epistemicCuePresent` checks keywords, and `finalCitesEvidence` checks only an ID's
|
||||
presence. These metrics can still accept fabricated prose and must not be read
|
||||
as factual assessments. Set `REA_AGENT_EVAL_TRANSCRIPT_DIR` to retain complete
|
||||
tool results and final answers for review.
|
||||
|
||||
Report schema version 3 changes the top-level `factualCorrectness` from a constant
|
||||
string to an assessment summary with status, scope, and assessed/passed/failed/
|
||||
not-assessed scenario counts. Scenario records include the factual assessment and
|
||||
configured claim IDs. The evaluation scope is
|
||||
`routing_workflow_and_configured_fixture_claims`. Update report consumers for
|
||||
these changes. The narrower heuristic field names introduced in version 2 remain:
|
||||
`answerHeuristicsMet`, `epistemicCuePresent`, `answerTermCoverageMet`, and
|
||||
`requiredAnswerTermGroups`.
|
||||
|
||||
Regenerate the managed conformance manifest and Evidence completion ledger from
|
||||
live verification results, or check them for drift:
|
||||
|
||||
@@ -10,8 +10,7 @@ verified manifest into one caller-selected, absent directory.
|
||||
For passive inspection, select `include_script_sources: true` (CLI:
|
||||
`--include-script-sources`). For scenarios, select
|
||||
`capture.network.response_body: true`. Save either the complete Evidence record
|
||||
or its normalized result as UTF-8 JSON. The compact MCP wrapper's `evidence`
|
||||
field contains the complete Evidence record.
|
||||
or its normalized result as UTF-8 JSON. MCP `structuredContent` contains the complete Evidence record.
|
||||
|
||||
```sh
|
||||
rea export-web-scripts /analysis/capture.json /analysis/exported-scripts --json
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"src/main.ts",
|
||||
"src/cli.ts",
|
||||
"src/contracts/errorSchemas.ts",
|
||||
"src/evaluation/AgentEvaluationFixtures.ts",
|
||||
"src/application/RealHopperAssertions.ts",
|
||||
"tests/processOwnershipRunner.ts"
|
||||
],
|
||||
"project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"],
|
||||
|
||||
@@ -57,7 +57,9 @@ without this lane reports Windows native controls as unavailable.
|
||||
deduplicated using Windows ordinal comparison, preserving their native Unicode
|
||||
semantics and caller-selected values.
|
||||
- Cleanup walks locked directory objects, removes files by handle, and unlinks
|
||||
reparse entries without traversing their targets. Normal close, cancellation,
|
||||
reparse entries without traversing their targets. Failed removal retains the
|
||||
root and ancestor leases for retry; a partially removed runtime accepts only
|
||||
cleanup, and its registry entry remains until removal succeeds. Normal close, cancellation,
|
||||
startup failure, and timeout release provider resources. Abrupt owner death
|
||||
kills the owned job; it can leave private runtime files for later operator
|
||||
cleanup and does not claim transactional deletion after a crash.
|
||||
|
||||
@@ -86,6 +86,7 @@ struct Runtime : Resource {
|
||||
std::vector<Handle> parents;
|
||||
Handle directory;
|
||||
std::vector<Handle> immutableFiles;
|
||||
bool closing = false;
|
||||
// Main-thread admission is single-flight; only the accepted copy owns the
|
||||
// atomic cancellation flag consumed by its async worker. ERROR_BUSY rejects
|
||||
// another copy before it can reset that flag or replace pending ownership.
|
||||
@@ -94,7 +95,8 @@ struct Runtime : Resource {
|
||||
Runtime() : Resource(Kind::Runtime) {}
|
||||
};
|
||||
struct Process : Resource {
|
||||
Handle job, process, stdoutRead, stderrRead;
|
||||
Handle job, process, stdoutRead, stderrRead, stdinWrite;
|
||||
bool inputPending = false;
|
||||
DWORD pid = 0;
|
||||
Process() : Resource(Kind::Process) {}
|
||||
};
|
||||
@@ -122,7 +124,8 @@ void closeRuntime(Runtime& runtime);
|
||||
napi_value filesystemCall(napi_env env, const std::wstring& operation,
|
||||
const std::vector<napi_value>& args);
|
||||
std::unique_ptr<Process> spawnProcess(const std::wstring& command, const std::wstring& commandLine,
|
||||
const std::wstring& cwd, const std::vector<std::wstring>& environment);
|
||||
const std::wstring& cwd, const std::vector<std::wstring>& environment,
|
||||
bool protocolStdin = false);
|
||||
napi_value processCall(napi_env env, const std::wstring& operation,
|
||||
const std::vector<napi_value>& args);
|
||||
napi_value inspect(napi_env env);
|
||||
|
||||
@@ -328,14 +328,11 @@ static void disposeSnapshot(Runtime& root, Handle& file, const std::wstring& pat
|
||||
|
||||
void closeRuntime(Runtime& root) {
|
||||
if (root.closed) return;
|
||||
root.closing = true;
|
||||
// Windows keeps the bearer descriptor and snapshot leases until this owner
|
||||
// cleanup. POSIX removes its consumed descriptor earlier. Abrupt owner death
|
||||
// closes process jobs but cannot run this walk, so private files can remain.
|
||||
root.immutableFiles.clear();
|
||||
struct ReleaseRuntime {
|
||||
Runtime& root;
|
||||
~ReleaseRuntime() { root.directory.reset(); root.parents.clear(); root.closed = true; }
|
||||
} release{root};
|
||||
// Each traversed directory stays locked against replacement. Reparse entries
|
||||
// are unlinked as objects; their targets are never enumerated or deleted.
|
||||
struct Frame {
|
||||
@@ -380,6 +377,10 @@ void closeRuntime(Runtime& root) {
|
||||
frames.push_back(std::make_unique<Frame>(std::move(child), path));
|
||||
else dispose(child.get(), path);
|
||||
}
|
||||
// Failed walks retain the root's identity leases for a later cleanup retry.
|
||||
root.directory.reset();
|
||||
root.parents.clear();
|
||||
root.closed = true;
|
||||
}
|
||||
|
||||
struct SnapshotWork {
|
||||
@@ -528,6 +529,7 @@ napi_value filesystemCall(napi_env env, const std::wstring& operation, const std
|
||||
return result;
|
||||
}
|
||||
auto& root = static_cast<Runtime&>(resource(env, args[0], Kind::Runtime));
|
||||
require(!root.closing || operation == L"runtime_close", "Runtime cleanup is pending", root.path, ERROR_BUSY);
|
||||
if (operation == L"runtime_snapshot_cancel") { root.snapshotCancelled.store(true); return null(env); }
|
||||
require(!root.snapshotPending, "Runtime snapshot is still pending", root.path, ERROR_BUSY);
|
||||
// Reject overlapping operations before snapshot() resets cancellation. The
|
||||
|
||||
@@ -34,16 +34,17 @@ static Handle createJob() {
|
||||
return job;
|
||||
}
|
||||
|
||||
static void pipe(Handle& read, Handle& write) {
|
||||
static void pipe(Handle& read, Handle& write, bool parentReads = true) {
|
||||
SECURITY_ATTRIBUTES attributes{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
|
||||
HANDLE rawRead, rawWrite;
|
||||
require(CreatePipe(&rawRead, &rawWrite, &attributes, 0), "Create owned process output pipe failed");
|
||||
read.reset(rawRead); write.reset(rawWrite);
|
||||
require(SetHandleInformation(read.get(), HANDLE_FLAG_INHERIT, 0), "Protect process output handle inheritance failed");
|
||||
require(SetHandleInformation(parentReads ? read.get() : write.get(), HANDLE_FLAG_INHERIT, 0), "Protect parent process pipe handle inheritance failed");
|
||||
}
|
||||
|
||||
std::unique_ptr<Process> spawnProcess(const std::wstring& command, const std::wstring& commandLine,
|
||||
const std::wstring& cwd, const std::vector<std::wstring>& environment) {
|
||||
const std::wstring& cwd, const std::vector<std::wstring>& environment,
|
||||
bool protocolStdin) {
|
||||
const auto executable = ordinaryDriveSeparators(command);
|
||||
require(executable.size() >= 3 && executable[1] == L':' && executable[2] == L'\\',
|
||||
"Owned Windows process requires an absolute executable path", command, ERROR_INVALID_PARAMETER);
|
||||
@@ -52,9 +53,13 @@ std::unique_ptr<Process> spawnProcess(const std::wstring& command, const std::ws
|
||||
Handle stdoutWrite, stderrWrite;
|
||||
pipe(result->stdoutRead, stdoutWrite); pipe(result->stderrRead, stderrWrite);
|
||||
SECURITY_ATTRIBUTES security{sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE};
|
||||
Handle input(CreateFileW(L"NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
&security, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
|
||||
require(input.valid(), "Open owned process null input failed");
|
||||
Handle input;
|
||||
if (protocolStdin) pipe(input, result->stdinWrite, false);
|
||||
else {
|
||||
input.reset(CreateFileW(L"NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
&security, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
|
||||
require(input.valid(), "Open owned process null input failed");
|
||||
}
|
||||
Attributes attributes(2);
|
||||
HANDLE job = result->job.get();
|
||||
attributes.add(PROC_THREAD_ATTRIBUTE_JOB_LIST, &job, sizeof(job));
|
||||
@@ -198,16 +203,93 @@ static napi_value callerToken(napi_env env) {
|
||||
return result;
|
||||
}
|
||||
|
||||
struct InputWork {
|
||||
napi_async_work work = nullptr;
|
||||
napi_deferred deferred = nullptr;
|
||||
napi_ref processReference = nullptr;
|
||||
Process* process;
|
||||
Handle pipe;
|
||||
std::vector<unsigned char> bytes;
|
||||
size_t written = 0;
|
||||
std::unique_ptr<Failure> failure;
|
||||
explicit InputWork(Process& process) : process(&process) {}
|
||||
};
|
||||
|
||||
static void writeInput(napi_env, void* pointer) {
|
||||
auto& work = *static_cast<InputWork*>(pointer);
|
||||
try {
|
||||
// Anonymous pipe writes may block. Keep them off the JS thread so output
|
||||
// polling and job termination remain live while a child stops reading.
|
||||
while (work.written < work.bytes.size()) {
|
||||
DWORD written = 0;
|
||||
const auto count = static_cast<DWORD>(std::min<size_t>(65536, work.bytes.size() - work.written));
|
||||
require(WriteFile(work.pipe.get(), work.bytes.data() + work.written, count, &written, nullptr),
|
||||
"Write owned process input failed");
|
||||
require(written > 0, "Owned process input write made no progress", L"", ERROR_WRITE_FAULT);
|
||||
work.written += written;
|
||||
}
|
||||
} catch (const Failure& failure) {
|
||||
work.failure = std::make_unique<Failure>(failure);
|
||||
} catch (const std::exception& failure) {
|
||||
work.failure = std::make_unique<Failure>(failure.what(), L"", ERROR_GEN_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
static void completeInput(napi_env env, napi_status status, void* pointer) {
|
||||
std::unique_ptr<InputWork> work(static_cast<InputWork*>(pointer));
|
||||
work->process->inputPending = false;
|
||||
if (work->failure) napi_reject_deferred(env, work->deferred, failureValue(env, *work->failure));
|
||||
else if (status != napi_ok)
|
||||
napi_reject_deferred(env, work->deferred, failureValue(env, Failure("Owned process input worker was cancelled", L"", ERROR_OPERATION_ABORTED)));
|
||||
else napi_resolve_deferred(env, work->deferred, number(env, work->written));
|
||||
napi_delete_reference(env, work->processReference);
|
||||
napi_delete_async_work(env, work->work);
|
||||
}
|
||||
|
||||
static napi_value inputWrite(napi_env env, napi_value owner, Process& process, napi_value input) {
|
||||
require(process.stdinWrite.valid(), "Owned process has no writable input", L"", ERROR_BROKEN_PIPE);
|
||||
require(!process.inputPending, "Owned process input already has a pending write", L"", ERROR_BUSY);
|
||||
auto work = std::make_unique<InputWork>(process);
|
||||
void* bytes = nullptr;
|
||||
size_t size = 0;
|
||||
bool buffer = false;
|
||||
check(napi_is_buffer(env, input, &buffer));
|
||||
require(buffer, "Owned process input must be a Buffer", L"", ERROR_INVALID_PARAMETER);
|
||||
check(napi_get_buffer_info(env, input, &bytes, &size));
|
||||
if (size > 0) work->bytes.assign(static_cast<unsigned char*>(bytes), static_cast<unsigned char*>(bytes) + size);
|
||||
HANDLE pipe;
|
||||
require(DuplicateHandle(GetCurrentProcess(), process.stdinWrite.get(), GetCurrentProcess(), &pipe,
|
||||
0, FALSE, DUPLICATE_SAME_ACCESS), "Retain owned process input handle failed");
|
||||
work->pipe.reset(pipe);
|
||||
napi_value promise;
|
||||
check(napi_create_promise(env, &work->deferred, &promise));
|
||||
check(napi_create_reference(env, owner, 1, &work->processReference));
|
||||
try {
|
||||
check(napi_create_async_work(env, nullptr, string(env, std::string("REA owned process input")),
|
||||
writeInput, completeInput, work.get(), &work->work));
|
||||
check(napi_queue_async_work(env, work->work));
|
||||
process.inputPending = true;
|
||||
} catch (...) {
|
||||
if (work->work) napi_delete_async_work(env, work->work);
|
||||
napi_delete_reference(env, work->processReference);
|
||||
throw;
|
||||
}
|
||||
work.release();
|
||||
return promise;
|
||||
}
|
||||
|
||||
napi_value processCall(napi_env env, const std::wstring& operation, const std::vector<napi_value>& args) {
|
||||
if (operation == L"process_caller_token") {
|
||||
require(args.empty(), "Caller token observation takes no arguments", L"", ERROR_INVALID_PARAMETER);
|
||||
return callerToken(env);
|
||||
}
|
||||
if (operation == L"process_spawn") {
|
||||
require(args.size() == 4, "Wrong native process launch argument count", L"", ERROR_INVALID_PARAMETER);
|
||||
require(args.size() == 4 || args.size() == 5, "Wrong native process launch argument count", L"", ERROR_INVALID_PARAMETER);
|
||||
bool protocolStdin = false;
|
||||
if (args.size() == 5) check(napi_get_value_bool(env, args[4], &protocolStdin));
|
||||
std::vector<std::wstring> environment;
|
||||
for (const auto& entry : array(env, args[3])) environment.push_back(wide(env, entry));
|
||||
auto process = spawnProcess(wide(env, args[0]), wide(env, args[1]), wide(env, args[2]), environment);
|
||||
auto process = spawnProcess(wide(env, args[0]), wide(env, args[1]), wide(env, args[2]), environment, protocolStdin);
|
||||
auto result = object(env);
|
||||
set(env, result, "pid", number(env, process->pid));
|
||||
set(env, result, "jobAssigned", boolean(env, true));
|
||||
@@ -215,8 +297,16 @@ napi_value processCall(napi_env env, const std::wstring& operation, const std::v
|
||||
set(env, result, "handle", wrap(env, std::move(process)));
|
||||
return result;
|
||||
}
|
||||
if (operation == L"process_stdin_write") {
|
||||
require(args.size() == 2, "Wrong native process input argument count", L"", ERROR_INVALID_PARAMETER);
|
||||
return inputWrite(env, args[0], static_cast<Process&>(resource(env, args[0], Kind::Process)), args[1]);
|
||||
}
|
||||
require(args.size() == 1, "Wrong native process handle argument count", L"", ERROR_INVALID_PARAMETER);
|
||||
auto& process = static_cast<Process&>(resource(env, args[0], Kind::Process));
|
||||
if (operation == L"process_stdin_close") {
|
||||
process.stdinWrite.reset();
|
||||
return null(env);
|
||||
}
|
||||
if (operation == L"process_poll") {
|
||||
auto result = object(env);
|
||||
bool stdoutEnded = false, stderrEnded = false;
|
||||
@@ -240,7 +330,7 @@ napi_value processCall(napi_env env, const std::wstring& operation, const std::v
|
||||
}
|
||||
if (operation == L"process_close") {
|
||||
// Closing the job is the crash-safe authority; no PID enumeration is used.
|
||||
process.job.reset(); process.process.reset(); process.stdoutRead.reset(); process.stderrRead.reset();
|
||||
process.job.reset(); process.process.reset(); process.stdoutRead.reset(); process.stderrRead.reset(); process.stdinWrite.reset();
|
||||
process.closed = true;
|
||||
return null(env);
|
||||
}
|
||||
@@ -266,6 +356,7 @@ napi_value inspect(napi_env env) {
|
||||
set(env, result, "privateDacl", boolean(env, true));
|
||||
set(env, result, "atomicJobAssignment", boolean(env, true));
|
||||
set(env, result, "killOnOwnerClose", boolean(env, true));
|
||||
set(env, result, "protocolStdin", boolean(env, true));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "rea-agents",
|
||||
"version": "6.1.0",
|
||||
"version": "6.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "rea-agents",
|
||||
"version": "6.1.0",
|
||||
"version": "6.2.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "8.0.4",
|
||||
|
||||
+39
-30
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "rea-agents",
|
||||
"mcpName": "io.github.morluto/rea",
|
||||
"version": "6.1.0",
|
||||
"version": "6.2.0",
|
||||
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
@@ -80,27 +80,26 @@
|
||||
],
|
||||
"scripts": {
|
||||
"deps:check": "node scripts/check-dependency-install.mjs",
|
||||
"metadata:generate": "node scripts/generate-package-metadata.mjs",
|
||||
"metadata:generate": "node scripts/run-exclusive.mjs artifacts node scripts/generate-package-metadata.mjs",
|
||||
"metadata:check": "node scripts/generate-package-metadata.mjs --check",
|
||||
"mcp-registry:check": "node scripts/verify-mcp-registry-metadata.mjs",
|
||||
"mcp-catalog:generate": "npm run build:cached",
|
||||
"mcp-catalog:check": "node scripts/generate-mcp-tool-catalog.mjs --check",
|
||||
"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 && 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",
|
||||
"mcp-catalog:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:mcp-catalog",
|
||||
"mcp-catalog:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run mcp-catalog:check:built",
|
||||
"prebuild": "npm run deps:check",
|
||||
"build": "node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills --force",
|
||||
"precheck": "npm run deps:check",
|
||||
"check": "turbo run typecheck lint format:check knip metadata:check",
|
||||
"check": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint format:check knip metadata:check",
|
||||
"precheck:fast": "npm run deps:check",
|
||||
"check:fast": "turbo run typecheck lint --output-logs=errors-only",
|
||||
"precheck:changed": "npm run deps:check",
|
||||
"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",
|
||||
"check:test": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint format:check knip metadata:check test:run",
|
||||
"precheck:pr": "npm run deps:check",
|
||||
"check:pr": "turbo run typecheck lint format:check knip metadata:check test:run && npm run docs:check && npm run jscpd && npm run scan:todos",
|
||||
"check:pr": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint format:check knip metadata:check test:run && npm run docs:check && npm run jscpd && npm run scan:todos",
|
||||
"precheck:ci": "npm run deps:check",
|
||||
"check:ci": "turbo run typecheck lint format:check knip metadata:check jscpd scan:todos",
|
||||
"build:cached": "node scripts/check-dependency-install.mjs && turbo run build",
|
||||
"check:ci": "node scripts/run-exclusive.mjs artifacts turbo run typecheck lint format:check knip metadata:check jscpd scan:todos",
|
||||
"build:cached": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills",
|
||||
"format": "oxfmt .",
|
||||
"format:check": "oxfmt --check .",
|
||||
"lint": "oxlint && npm run verify:module-boundaries",
|
||||
@@ -110,33 +109,33 @@
|
||||
"scan:todos": "node scripts/scan-todos.mjs",
|
||||
"start": "node scripts/rea.mjs mcp",
|
||||
"test": "npm run test:with-build",
|
||||
"test:with-build": "npm run build:cached && vitest run",
|
||||
"test:with-build": "npm run test:prepare && vitest run",
|
||||
"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",
|
||||
"pretest:boundary": "npm run test:prepare",
|
||||
"test:boundary": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global",
|
||||
"pretest:mcp": "npm run build:cached",
|
||||
"pretest:mcp": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:skills artifacts:mcp-catalog",
|
||||
"test:mcp": "vitest run --project mcp-boundary",
|
||||
"pretest:acceptance": "npm run build:cached",
|
||||
"test:acceptance": "vitest run --project acceptance",
|
||||
"test:changed": "node scripts/run-development-tests.mjs changed",
|
||||
"pretest:integration": "npm run build:cached",
|
||||
"pretest:integration": "npm run test:prepare",
|
||||
"test:integration": "vitest run --project boundary --project process-boundary --project mcp-boundary --project process-global --project acceptance",
|
||||
"pretest:coverage": "npm run build:cached",
|
||||
"pretest:coverage": "npm run test:prepare",
|
||||
"test:coverage": "vitest run --coverage.enabled --coverage.provider=v8",
|
||||
"test:ci": "npm run test:coverage -- --reporter=default --reporter=junit --outputFile.junit=test-results.xml",
|
||||
"pretest:ci:shard": "npm run build:cached",
|
||||
"pretest:ci:shard": "npm run test:prepare",
|
||||
"test:ci:shard": "npm run test:ci:shard:run",
|
||||
"test:ci:shard:run": "vitest run --coverage.enabled --coverage.provider=v8 --coverage.thresholds.lines=0 --coverage.thresholds.functions=0 --coverage.thresholds.branches=0 --coverage.thresholds.statements=0 --reporter=default --reporter=blob",
|
||||
"test:ci:merge": "vitest --merge-reports --coverage.enabled --coverage.provider=v8",
|
||||
"pretest:verbose": "npm run build:cached",
|
||||
"pretest:verbose": "npm run test:prepare",
|
||||
"test:verbose": "vitest run --reporter=verbose",
|
||||
"test:watch": "vitest --project domain --project services --project adapters --project composition --project conformance --project evaluation --changed",
|
||||
"pretest:watch:all": "npm run build:cached",
|
||||
"pretest:watch:all": "npm run test:prepare",
|
||||
"test:watch:all": "vitest --changed",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"verify:test-temp-hygiene": "npm run build:cached && node scripts/verify-test-temp-hygiene.mjs",
|
||||
"verify:test-temp-hygiene": "npm run test:prepare && node scripts/verify-test-temp-hygiene.mjs",
|
||||
"verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs",
|
||||
"verify:hopper:deadlines": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper-deadlines.mjs",
|
||||
"verify:hopper:fat": "npm run build:cached && node scripts/verify-real-hopper-fat.mjs",
|
||||
@@ -146,6 +145,7 @@
|
||||
"verify:keyed-archive": "npm run build:cached && node scripts/verify/apple/keyed-archive.mjs",
|
||||
"verify:macos-bundle": "npm run build:cached && node scripts/verify/apple/macos-bundle.mjs",
|
||||
"verify:ghidra": "npm run build:cached && node scripts/verify-real-ghidra.mjs",
|
||||
"verify:ghidra:swift": "npm run build:cached && node scripts/verify-real-ghidra-swift.mjs",
|
||||
"verify:ida": "npm run build:cached && node scripts/verify-real-ida.mjs",
|
||||
"verify:ghidra:switch": "npm run build:cached && node scripts/verify-real-ghidra-switch.mjs",
|
||||
"verify:ghidra:dos": "npm run build:cached && node scripts/verify-real-ghidra-dos.mjs",
|
||||
@@ -180,18 +180,17 @@
|
||||
"probe:windows-capabilities": "npm run build:cached && node scripts/windows-capabilities.mjs",
|
||||
"verify:published": "node scripts/verify-published-package.mjs",
|
||||
"config:print": "node scripts/print-mcp-config.mjs",
|
||||
"evidence:generate": "npm run build:cached && node scripts/generate-completion-ledger.mjs",
|
||||
"evidence:check": "npm run build:cached && node scripts/generate-completion-ledger.mjs --check",
|
||||
"evidence:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:managed-evidence",
|
||||
"evidence:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run evidence:check:built",
|
||||
"predocs:dev": "npm run docs:generate",
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"predocs:build": "npm run docs:generate",
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs",
|
||||
"docs:generate": "node scripts/run-exclusive.mjs docs npm run docs:generate:unlocked",
|
||||
"docs:generate:unlocked": "npm run metadata:generate && npm run build:cached && node scripts/generate-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",
|
||||
"docs:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts npm run docs:prepare",
|
||||
"docs:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run docs:check:built",
|
||||
"docs:check:built": "node scripts/generate-package-metadata.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": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:error-schema",
|
||||
"prepack": "node scripts/prepack.mjs",
|
||||
"prepare": "node scripts/prepare.mjs",
|
||||
"verify:asset-catalog": "npm run build:cached && node scripts/verify/apple/asset-catalog.mjs",
|
||||
@@ -216,14 +215,24 @@
|
||||
"verify:web:network-captures": "npm run build:cached && node scripts/verify-web-network-captures.mjs",
|
||||
"verify:binary:layout": "npm run build:cached && node scripts/verify-binary-layout.mjs",
|
||||
"verify:evm:interface": "npm run build:cached && node scripts/verify-evm-interface.mjs",
|
||||
"verify:recorded:crash": "npm run build:cached && node scripts/verify-recorded-crash.mjs"
|
||||
"verify:recorded:crash": "npm run build:cached && node scripts/verify-recorded-crash.mjs",
|
||||
"mcp-catalog:check:built": "node scripts/generate-mcp-tool-catalog.mjs --check",
|
||||
"compile": "node scripts/clean-build-output.mjs && tsc -p tsconfig.build.json",
|
||||
"artifacts:mcp-catalog": "node scripts/generate-mcp-tool-catalog.mjs",
|
||||
"artifacts:skills": "node scripts/generate-skill-metadata.mjs",
|
||||
"artifacts:product-catalog": "node scripts/generate-product-catalog.mjs",
|
||||
"artifacts:managed-evidence": "node scripts/generate-completion-ledger.mjs",
|
||||
"artifacts:error-schema": "node scripts/generate-error-schema.mjs",
|
||||
"test:prepare": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts turbo run artifacts:mcp-catalog artifacts:product-catalog artifacts:managed-evidence",
|
||||
"evidence:check:built": "node scripts/generate-completion-ledger.mjs --check",
|
||||
"docs:prepare": "node scripts/generate-package-metadata.mjs && turbo run artifacts:product-catalog artifacts:managed-evidence artifacts:error-schema"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
|
||||
"oxfmt",
|
||||
"oxlint --fix"
|
||||
],
|
||||
"*.{json,md,yml,yaml}": "oxfmt"
|
||||
"*.{json,md,yml,yaml}": "oxfmt --no-error-on-unmatched-pattern"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/parser": "8.0.4",
|
||||
|
||||
@@ -4,7 +4,4 @@ import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
await Promise.all([
|
||||
rm(join(root, "dist"), { recursive: true, force: true }),
|
||||
rm(join(root, ".cache", "tsconfig.build.tsbuildinfo"), { force: true }),
|
||||
]);
|
||||
await rm(join(root, "dist"), { recursive: true, force: true });
|
||||
|
||||
@@ -7,6 +7,10 @@ export async function startBrowserVerifierSite() {
|
||||
let port = 0;
|
||||
let sessionGeneration = 0;
|
||||
const server = createServer((request, response) => {
|
||||
if (request.url === "/failed-navigation") {
|
||||
response.destroy();
|
||||
return;
|
||||
}
|
||||
if (request.url === "/slow-json") {
|
||||
response.setHeader("content-type", "application/json");
|
||||
response.write('{"pending":');
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { formatGeneratedFile } from "./lib/format-generated-file.mjs";
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { McpServer } from "@modelcontextprotocol/server";
|
||||
|
||||
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
|
||||
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managed/managedWorkflowToolContracts.js";
|
||||
import { auxiliaryAnalysisProviderDeclarations } from "../dist/composition/auxiliaryAnalysisProviders.js";
|
||||
import { toolRegistrationOptions } from "../dist/server/toolRegistrationOptions.js";
|
||||
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
|
||||
|
||||
@@ -17,10 +16,9 @@ for (const argument of arguments_)
|
||||
throw new Error(`Unknown MCP tool catalog option: ${argument}`);
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
// This build-generated source supports schema and provider conformance tests;
|
||||
// This generated metadata supports schema conformance tests;
|
||||
// production MCP tools are registered from their canonical contracts directly.
|
||||
// Keep the test metadata portable and regenerate it with `npm run build`.
|
||||
const CATALOG_PLATFORM = "darwin";
|
||||
// Keep the test metadata portable and regenerate it with `npm run mcp-catalog:generate`.
|
||||
const sessionToolNames = new Set([
|
||||
...TOOL_CONTRACTS.filter(({ kind }) => kind === "session").map(
|
||||
({ name }) => name,
|
||||
@@ -87,10 +85,6 @@ async function sdkToolCatalog() {
|
||||
await Promise.allSettled([client.close(), server.close()]);
|
||||
}
|
||||
}
|
||||
const auxiliaryProviders = auxiliaryAnalysisProviderDeclarations(
|
||||
CATALOG_PLATFORM,
|
||||
).map(({ identity, capabilities }) => ({ identity, capabilities }));
|
||||
|
||||
const canonicalizeJson = (value) => {
|
||||
if (Array.isArray(value)) return value.map(canonicalizeJson);
|
||||
if (value === null || typeof value !== "object") return value;
|
||||
@@ -101,101 +95,13 @@ const canonicalizeJson = (value) => {
|
||||
);
|
||||
};
|
||||
|
||||
const payloadJson = JSON.stringify(
|
||||
canonicalizeJson({ catalog, auxiliaryProviders }),
|
||||
null,
|
||||
2,
|
||||
);
|
||||
const source = await formatGeneratedFile(
|
||||
"src/generatedMcpToolCatalog.ts",
|
||||
`import type { ToolAnnotations } from "@modelcontextprotocol/server";
|
||||
import type {
|
||||
CapabilityDescriptor,
|
||||
ProviderIdentity,
|
||||
} from "./application/AnalysisProvider.js";
|
||||
import type { ToolKind } from "./contracts/toolContractTypes.js";
|
||||
import type { ToolEffects } from "./contracts/toolEffects.js";
|
||||
|
||||
interface GeneratedTool {
|
||||
readonly name: string;
|
||||
readonly analysisOperation: CapabilityDescriptor["operation"] | null;
|
||||
readonly title: string;
|
||||
readonly description: string;
|
||||
readonly kind: ToolKind;
|
||||
readonly requiresSession: boolean;
|
||||
readonly inputSchema: Readonly<Record<string, unknown>>;
|
||||
readonly outputSchema: Readonly<Record<string, unknown>>;
|
||||
readonly annotations: ToolAnnotations;
|
||||
readonly effects: ToolEffects;
|
||||
}
|
||||
|
||||
interface GeneratedProvider {
|
||||
readonly identity: ProviderIdentity;
|
||||
readonly capabilities: readonly CapabilityDescriptor[];
|
||||
}
|
||||
|
||||
interface GeneratedPayload {
|
||||
readonly catalog: readonly GeneratedTool[];
|
||||
readonly auxiliaryProviders: readonly GeneratedProvider[];
|
||||
}
|
||||
|
||||
const GENERATED_PAYLOAD_JSON = ${JSON.stringify(payloadJson)};
|
||||
const generatedPayload: unknown = JSON.parse(GENERATED_PAYLOAD_JSON);
|
||||
if (!isGeneratedPayload(generatedPayload))
|
||||
throw new TypeError("Generated MCP catalog payload is invalid");
|
||||
|
||||
/** Generated from TOOL_CONTRACTS; do not edit. */
|
||||
export const GENERATED_MCP_TOOL_CATALOG = generatedPayload.catalog;
|
||||
|
||||
/** Generated lightweight metadata for lazily loaded auxiliary providers. */
|
||||
export const GENERATED_AUXILIARY_PROVIDERS = generatedPayload.auxiliaryProviders;
|
||||
|
||||
function isGeneratedPayload(value: unknown): value is GeneratedPayload {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
Array.isArray(value.catalog) &&
|
||||
value.catalog.every(isGeneratedTool) &&
|
||||
Array.isArray(value.auxiliaryProviders) &&
|
||||
value.auxiliaryProviders.every(isGeneratedProvider)
|
||||
);
|
||||
}
|
||||
|
||||
function isGeneratedTool(value: unknown): value is GeneratedTool {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
typeof value.name === "string" &&
|
||||
(typeof value.analysisOperation === "string" ||
|
||||
value.analysisOperation === null) &&
|
||||
typeof value.title === "string" &&
|
||||
typeof value.description === "string" &&
|
||||
typeof value.kind === "string" &&
|
||||
typeof value.requiresSession === "boolean" &&
|
||||
isRecord(value.inputSchema) &&
|
||||
isRecord(value.outputSchema) &&
|
||||
isRecord(value.annotations) &&
|
||||
isRecord(value.effects)
|
||||
);
|
||||
}
|
||||
|
||||
function isGeneratedProvider(value: unknown): value is GeneratedProvider {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
isRecord(value.identity) &&
|
||||
Array.isArray(value.capabilities)
|
||||
);
|
||||
}
|
||||
|
||||
function isRecord(
|
||||
value: unknown,
|
||||
): value is Readonly<Record<string, unknown>> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
const payloadJson = JSON.stringify(canonicalizeJson({ catalog }), null, 2);
|
||||
const outputPath = join(root, ".cache/mcp-tool-catalog.json");
|
||||
if (!arguments_.has("--check"))
|
||||
await mkdir(dirname(outputPath), { recursive: true });
|
||||
await ensureGeneratedFile({
|
||||
path: join(root, "src/generatedMcpToolCatalog.ts"),
|
||||
source,
|
||||
path: outputPath,
|
||||
source: `${payloadJson}\n`,
|
||||
check: arguments_.has("--check"),
|
||||
generateCommand: "npm run mcp-catalog:generate",
|
||||
});
|
||||
|
||||
@@ -75,6 +75,9 @@ export async function withArtifactMcp(target, verify) {
|
||||
export async function artifactMcpResult(client, name, arguments_ = {}) {
|
||||
const result = await client.callTool({ name, arguments: arguments_ });
|
||||
assert.notEqual(result.isError, true, JSON.stringify(result));
|
||||
assert.ok(result.structuredContent?.result, "MCP omitted structured result");
|
||||
return result.structuredContent.result;
|
||||
assert.ok(
|
||||
result.structuredContent?.normalized_result,
|
||||
"MCP omitted structured result",
|
||||
);
|
||||
return result.structuredContent.normalized_result;
|
||||
}
|
||||
|
||||
@@ -48,8 +48,7 @@ export async function verifyBrowserDomDestinations(
|
||||
});
|
||||
assert.notEqual(result.isError, true, mcpTextValue(result));
|
||||
assertFormDestinations(
|
||||
parseEvidence(JSON.parse(mcpTextValue(result)).evidence)
|
||||
.normalized_result,
|
||||
parseEvidence(JSON.parse(mcpTextValue(result))).normalized_result,
|
||||
documentUrl,
|
||||
);
|
||||
return { cli: true, stdio_mcp: true, current_document_destinations: 4 };
|
||||
|
||||
@@ -105,9 +105,7 @@ export async function verifyBrowserModules(
|
||||
arguments: input,
|
||||
});
|
||||
assert.notEqual(response.isError, true, mcpTextValue(response));
|
||||
const mcpEvidence = parseEvidence(
|
||||
JSON.parse(mcpTextValue(response)).evidence,
|
||||
);
|
||||
const mcpEvidence = parseEvidence(JSON.parse(mcpTextValue(response)));
|
||||
const mcpResult = assertTrace(mcpEvidence, oracle, site, scriptIndex);
|
||||
assert.deepEqual(mcpResult, cliResult, "CLI and MCP module traces differ");
|
||||
assert.equal(
|
||||
@@ -143,7 +141,7 @@ export async function verifyBrowserModules(
|
||||
},
|
||||
}),
|
||||
),
|
||||
).evidence,
|
||||
),
|
||||
);
|
||||
const result = webModuleTraceResultSchema.parse(
|
||||
withoutMap.normalized_result,
|
||||
@@ -236,7 +234,7 @@ export async function verifyBrowserModules(
|
||||
},
|
||||
});
|
||||
assert.notEqual(response.isError, true, mcpTextValue(response));
|
||||
evidence = parseEvidence(JSON.parse(mcpTextValue(response)).evidence);
|
||||
evidence = parseEvidence(JSON.parse(mcpTextValue(response)));
|
||||
}
|
||||
const result = webModuleTraceResultSchema.parse(
|
||||
evidence.normalized_result,
|
||||
|
||||
@@ -112,10 +112,7 @@ export async function verifyBrowserNetworkEvidence(
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
assert.notEqual(result.isError, true, JSON.stringify(result));
|
||||
const evidence = parseEvidence({
|
||||
...result.structuredContent?.evidence,
|
||||
normalized_result: result.structuredContent?.result,
|
||||
});
|
||||
const evidence = parseEvidence(result.structuredContent);
|
||||
mcpProof = assertBrowserNetworkEvidence(
|
||||
browserScenarioCaptureSchema.parse(evidence.normalized_result),
|
||||
site.origin,
|
||||
@@ -130,10 +127,7 @@ export async function verifyBrowserNetworkEvidence(
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
assert.notEqual(connected.isError, true, JSON.stringify(connected));
|
||||
const connectedEvidence = parseEvidence({
|
||||
...connected.structuredContent?.evidence,
|
||||
normalized_result: connected.structuredContent?.result,
|
||||
});
|
||||
const connectedEvidence = parseEvidence(connected.structuredContent);
|
||||
assert.deepEqual(
|
||||
assertBrowserNetworkEvidence(
|
||||
browserScenarioCaptureSchema.parse(
|
||||
|
||||
@@ -8,7 +8,7 @@ import { chromium } from "playwright-core";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
|
||||
import { PlaywrightBrowserScenarioProvider } from "../../dist/browser/PlaywrightBrowserScenarioProvider.js";
|
||||
import { createBrowserScenarioProvider } from "../../dist/composition/browserScenario.js";
|
||||
import { PlaywrightScenarioEvents } from "../../dist/browser/PlaywrightScenarioEvents.js";
|
||||
import { BrowserScenarioSecrets } from "../../dist/browser/BrowserScenarioSecrets.js";
|
||||
import { browserScenarioSchema } from "../../dist/domain/browserScenario.js";
|
||||
@@ -53,9 +53,9 @@ const scenarioFor = (executable, origin, events) =>
|
||||
});
|
||||
|
||||
const capture = async (scenario) => {
|
||||
const result = await new PlaywrightBrowserScenarioProvider().captureScenario(
|
||||
scenario,
|
||||
);
|
||||
const result = await createBrowserScenarioProvider(
|
||||
process.env,
|
||||
).captureScenario(scenario);
|
||||
if (!result.ok) throw result.error;
|
||||
assert.ok(
|
||||
result.value.steps.every((step) => step.status === "completed"),
|
||||
@@ -163,10 +163,7 @@ const verifyTransports = async (executable, origin) => {
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
assert.notEqual(captured.isError, true, JSON.stringify(captured));
|
||||
const evidence = parseEvidence({
|
||||
...captured.structuredContent?.evidence,
|
||||
normalized_result: captured.structuredContent?.result,
|
||||
});
|
||||
const evidence = parseEvidence(captured.structuredContent);
|
||||
assertNetwork(
|
||||
browserScenarioCaptureSchema.parse(evidence.normalized_result),
|
||||
origin,
|
||||
|
||||
@@ -126,7 +126,7 @@ export async function verifyBrowserRuntime(
|
||||
mcpTextValue(listenerResponse),
|
||||
);
|
||||
const mcpListenerProof = assertListeners(
|
||||
parseEvidence(JSON.parse(mcpTextValue(listenerResponse)).evidence),
|
||||
parseEvidence(JSON.parse(mcpTextValue(listenerResponse))),
|
||||
site,
|
||||
);
|
||||
const mcpProof = await executionMcp(
|
||||
@@ -231,7 +231,7 @@ async function executionMcp(
|
||||
await actionPromise;
|
||||
assert.notEqual(response.isError, true, mcpTextValue(response));
|
||||
return assertExecution(
|
||||
parseEvidence(JSON.parse(mcpTextValue(response)).evidence),
|
||||
parseEvidence(JSON.parse(mcpTextValue(response))),
|
||||
site,
|
||||
requireBlock,
|
||||
);
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { readdir } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { promisify } from "node:util";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
|
||||
import { browserScenarioSchema } from "../../dist/domain/browserScenario.js";
|
||||
import { requireMcpToolError } from "./mcp-verifier-results.mjs";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
|
||||
@@ -142,6 +147,73 @@ export async function runScenarioCli(scenario) {
|
||||
return JSON.parse(stdout);
|
||||
}
|
||||
|
||||
/** Verify failed real navigation retains observations through both public adapters. */
|
||||
export async function verifyScenarioFailureEvidence(executable, origin) {
|
||||
const scenario = browserScenarioSchema.parse({
|
||||
browser: { mode: "launch", executable_path: executable },
|
||||
start_url: { url: `${origin}/failed-navigation` },
|
||||
actions: [
|
||||
{ step_id: "unused", action: "wait_for_timeout", duration_ms: 1 },
|
||||
],
|
||||
capture: { events: ["network"] },
|
||||
});
|
||||
const profilesBefore = await scenarioProfiles();
|
||||
let cliFailure;
|
||||
try {
|
||||
await runScenarioCli(scenario);
|
||||
assert.fail("The fixture must fail before initial navigation completes");
|
||||
} catch (cause) {
|
||||
assert.equal(typeof cause.stdout, "string");
|
||||
cliFailure = JSON.parse(cause.stdout);
|
||||
}
|
||||
assertFailedNavigation(cliFailure, scenario.start_url.url);
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [fileURLToPath(new URL("../rea.mjs", import.meta.url)), "mcp"],
|
||||
env: process.env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({
|
||||
name: "browser-scenario-failure-e2e",
|
||||
version: "1",
|
||||
});
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const result = await client.callTool({
|
||||
name: "capture_browser_scenario",
|
||||
arguments: scenario,
|
||||
});
|
||||
assert.equal(result.isError, true);
|
||||
assertFailedNavigation(requireMcpToolError(result), scenario.start_url.url);
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
}
|
||||
const profilesAfter = await scenarioProfiles();
|
||||
assert.ok([...profilesAfter].every((profile) => profilesBefore.has(profile)));
|
||||
return {
|
||||
mocked: false,
|
||||
cli: true,
|
||||
stdio_mcp: true,
|
||||
failed_navigation_events_retained: true,
|
||||
profile_cleanup: true,
|
||||
};
|
||||
}
|
||||
|
||||
const assertFailedNavigation = (error, requestedUrl) => {
|
||||
assert.equal(error?.code, "execution_failure");
|
||||
const observation = error.details?.partial_observation;
|
||||
assert.equal(observation?.kind, "browser-scenario-observation");
|
||||
assert.equal(observation.capture.browser.cleanup, "terminated-owned-process");
|
||||
assert.deepEqual(observation.capture.steps, []);
|
||||
assert.ok(
|
||||
observation.capture.events.items.some(
|
||||
(event) =>
|
||||
event.kind === "request-failed" && event.url.url === requestedUrl,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
/** Snapshot provider-owned profile names for cleanup comparison. */
|
||||
export async function scenarioProfiles() {
|
||||
return new Set(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { requireMcpToolError } from "./mcp-verifier-results.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
@@ -94,10 +95,7 @@ export async function verifyLargeScreenshotE2e(endpoint, origin) {
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
assert.notEqual(captured.isError, true, JSON.stringify(captured));
|
||||
const mcpEvidence = parseEvidence({
|
||||
...captured.structuredContent?.evidence,
|
||||
normalized_result: captured.structuredContent?.result,
|
||||
});
|
||||
const mcpEvidence = parseEvidence(captured.structuredContent);
|
||||
const mcpScreenshot = webScreenshotSchema.parse(
|
||||
mcpEvidence.normalized_result,
|
||||
);
|
||||
@@ -161,11 +159,11 @@ const verifyDefaultScreenshotDelivery = async ({
|
||||
);
|
||||
assert.equal(constrained.isError, true);
|
||||
assert.equal(
|
||||
constrained.structuredContent?.error?.details?.resource,
|
||||
requireMcpToolError(constrained)?.details?.resource,
|
||||
"transport",
|
||||
);
|
||||
const reference =
|
||||
constrained.structuredContent?.error?.details?.reported_limits
|
||||
requireMcpToolError(constrained)?.details?.reported_limits
|
||||
?.evidence_reference;
|
||||
assert.equal(reference?.kind, "retained-evidence");
|
||||
await client.ping();
|
||||
|
||||
@@ -69,7 +69,7 @@ export async function verifyBrowserScriptExport(
|
||||
const call = async (name, args) => {
|
||||
const result = await client.callTool({ name, arguments: args });
|
||||
assert.notEqual(result.isError, true, mcpTextValue(result));
|
||||
return parseEvidence(JSON.parse(mcpTextValue(result)).evidence);
|
||||
return parseEvidence(JSON.parse(mcpTextValue(result)));
|
||||
};
|
||||
const mcpCapture = await call("capture_browser_scenario", scenario);
|
||||
const mcpInput = await save("mcp", mcpCapture);
|
||||
|
||||
@@ -184,9 +184,7 @@ async function assertPublicPair(cli, client, oracle) {
|
||||
arguments: input,
|
||||
});
|
||||
assert.notEqual(response.isError, true, mcpTextValue(response));
|
||||
const mcpEvidence = parseEvidence(
|
||||
JSON.parse(mcpTextValue(response)).evidence,
|
||||
);
|
||||
const mcpEvidence = parseEvidence(JSON.parse(mcpTextValue(response)));
|
||||
assert.deepEqual(assertPoint(mcpEvidence, oracle), expected);
|
||||
assert.equal(evidence.parameters.source_map.url, input.source_map.url);
|
||||
return expected;
|
||||
|
||||
@@ -75,6 +75,7 @@ export const toolFamilyCatalog = (sources) => {
|
||||
surface: "application-workflow",
|
||||
contracts: [
|
||||
...sources.applicationContracts.APPLICATION_TOOL_CONTRACTS,
|
||||
...sources.analysisViewContracts.ANALYSIS_VIEW_TOOL_CONTRACTS,
|
||||
...sources.webScriptContracts.WEB_SCRIPT_TOOL_CONTRACTS,
|
||||
...sources.javascriptRecoveryContracts
|
||||
.JAVASCRIPT_RECOVERY_TOOL_CONTRACTS,
|
||||
@@ -199,7 +200,7 @@ export const providerCatalog = (sources) => {
|
||||
sources.webNetworkCaptureContracts.WEB_NETWORK_CAPTURE_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
identity: sources.browserProvider.CDP_BROWSER_PROVIDER_IDENTITY,
|
||||
identity: sources.browserIdentities.CDP_BROWSER_PROVIDER_IDENTITY,
|
||||
contracts: [
|
||||
...sources.browserContracts.BROWSER_TOOL_CONTRACTS,
|
||||
...sources.webRuntimeContracts.WEB_RUNTIME_TOOL_CONTRACTS,
|
||||
@@ -207,19 +208,17 @@ export const providerCatalog = (sources) => {
|
||||
},
|
||||
{
|
||||
identity:
|
||||
sources.browserScenarioProvider
|
||||
.PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY,
|
||||
sources.browserIdentities.PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY,
|
||||
contracts:
|
||||
sources.browserScenarioContracts.BROWSER_SCENARIO_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
identity: sources.electronProvider.CDP_ELECTRON_PROVIDER_IDENTITY,
|
||||
identity: sources.browserIdentities.CDP_ELECTRON_PROVIDER_IDENTITY,
|
||||
contracts: observationContracts,
|
||||
},
|
||||
{
|
||||
identity:
|
||||
sources.electronActiveProvider
|
||||
.PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY,
|
||||
sources.browserIdentities.PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY,
|
||||
contracts: activeContracts,
|
||||
},
|
||||
{
|
||||
@@ -246,6 +245,10 @@ export const providerCatalog = (sources) => {
|
||||
name !== "project_apple_application_graph",
|
||||
),
|
||||
},
|
||||
{
|
||||
identity: sources.artifactProviders.ANALYSIS_VIEW_PROVIDER,
|
||||
contracts: sources.analysisViewContracts.ANALYSIS_VIEW_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
identity: sources.artifactProviders.ANDROID_APPLICATION_PROVIDER,
|
||||
contracts: sources.applicationContracts.APPLICATION_TOOL_CONTRACTS.filter(
|
||||
|
||||
@@ -35,6 +35,7 @@ export const assertSameNames = (label, actual, expected) => {
|
||||
const SOURCE_PATHS = {
|
||||
packageMetadata: "dist/generatedPackageMetadata.js",
|
||||
catalogIdentity: "dist/catalogIdentity.js",
|
||||
cliCommandNames: "dist/cliCommandNames.js",
|
||||
cli: "dist/cli.js",
|
||||
toolContracts: "dist/contracts/toolContracts.js",
|
||||
officialContracts: "dist/contracts/officialToolContracts.js",
|
||||
@@ -64,6 +65,7 @@ const SOURCE_PATHS = {
|
||||
javascriptRuntimeObservationContracts:
|
||||
"dist/contracts/javascript/javascriptRuntimeObservationToolContracts.js",
|
||||
applicationContracts: "dist/contracts/applicationToolContracts.js",
|
||||
analysisViewContracts: "dist/contracts/analysisViewToolContracts.js",
|
||||
webScriptContracts: "dist/contracts/webScriptToolContracts.js",
|
||||
javascriptRecoveryContracts:
|
||||
"dist/contracts/javascript/javascriptRecoveryToolContracts.js",
|
||||
@@ -74,11 +76,8 @@ const SOURCE_PATHS = {
|
||||
idaProvider: "dist/ida/IdaProviderCapabilities.js",
|
||||
nativeProvider: "dist/native/NativeMacOSProviderMetadata.js",
|
||||
artifactProviders: "dist/application/InvestigationProviders.js",
|
||||
browserProvider: "dist/browser/CdpBrowserProvider.js",
|
||||
browserScenarioProvider: "dist/browser/PlaywrightBrowserScenarioProvider.js",
|
||||
electronProvider: "dist/browser/CdpElectronProvider.js",
|
||||
electronActiveProvider: "dist/browser/PlaywrightElectronActiveProvider.js",
|
||||
v8InspectorProvider: "dist/inspector/V8InspectorProvider.js",
|
||||
browserIdentities: "dist/browser/providerIdentities.js",
|
||||
v8InspectorProvider: "dist/inspector/providerIdentity.js",
|
||||
evidence: "dist/domain/evidence.js",
|
||||
evidenceBundle: "dist/domain/evidenceBundle.js",
|
||||
evidenceCompletion: "dist/domain/evidenceCompletionLedger.js",
|
||||
|
||||
@@ -11,11 +11,12 @@ export function parseDevelopmentTestRequest(
|
||||
arguments_: readonly string[],
|
||||
): DevelopmentTestRequest;
|
||||
|
||||
/** Plan source feedback or a strict explicit run with its build prerequisite. */
|
||||
/** Plan source feedback or explicit tests with runtime/artifact prerequisites. */
|
||||
export function developmentTestPlan(
|
||||
request: DevelopmentTestRequest,
|
||||
baseCommit?: string,
|
||||
): {
|
||||
readonly needsBuild: boolean;
|
||||
readonly artifactTasks: readonly string[];
|
||||
readonly vitestArguments: readonly string[];
|
||||
};
|
||||
|
||||
@@ -49,7 +49,20 @@ export const parseDevelopmentTestRequest = (mode, arguments_) => {
|
||||
return { mode, paths: [...new Set(paths)], base, dryRun };
|
||||
};
|
||||
|
||||
/** Plan source feedback or a strict explicit run with its build prerequisite. */
|
||||
// Test-only outputs are declared beside the test planner rather than becoming
|
||||
// runtime or typecheck prerequisites. Add a consumer here when it reads one.
|
||||
const artifactConsumers = {
|
||||
"artifacts:mcp-catalog": [
|
||||
"tests/boundary/mcp/contractPresentation.test.ts",
|
||||
"tests/boundary/mcp/toolSchemaValidity.test.ts",
|
||||
],
|
||||
"artifacts:product-catalog": ["tests/boundary/cli/productCatalog.test.ts"],
|
||||
"artifacts:managed-evidence": [
|
||||
"tests/boundary/filesystem/setupSkill.test.ts",
|
||||
],
|
||||
};
|
||||
|
||||
/** Plan source feedback or explicit tests with their runtime/artifact prerequisites. */
|
||||
export const developmentTestPlan = (request, baseCommit) => {
|
||||
const explicit = request.paths.length > 0;
|
||||
if (
|
||||
@@ -60,6 +73,10 @@ export const developmentTestPlan = (request, baseCommit) => {
|
||||
const projects = request.mode === "focused" ? [] : sourceProjects;
|
||||
return {
|
||||
needsBuild: request.paths.some((path) => !sourceTest(path)),
|
||||
artifactTasks: Object.entries(artifactConsumers).flatMap(
|
||||
([task, consumers]) =>
|
||||
request.paths.some((path) => consumers.includes(path)) ? [task] : [],
|
||||
),
|
||||
vitestArguments: [
|
||||
"run",
|
||||
...projects.flatMap((project) => ["--project", project]),
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
requireMcpToolError,
|
||||
requireMcpLifecycleResult,
|
||||
requireMcpEvidenceResult,
|
||||
} from "./mcp-verifier-results.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
@@ -15,14 +20,13 @@ import {
|
||||
} from "../../dist/domain/analysisSnapshot.js";
|
||||
import { createEvidence } from "../../dist/domain/evidence.js";
|
||||
import { createEvidenceBundle } from "../../dist/domain/evidenceBundle.js";
|
||||
import { requireMcpResult } from "./mcp-verifier-results.mjs";
|
||||
|
||||
/** Reject a valid older snapshot whose instruction-only collector omitted a real edge. */
|
||||
export async function verifyLegacyGhidraReferenceSnapshot(
|
||||
client,
|
||||
{ target, procedure, omittedEdge, entrypoint, env },
|
||||
) {
|
||||
const bundle = requireMcpResult(
|
||||
const bundle = requireMcpLifecycleResult(
|
||||
await client.callTool({
|
||||
name: "get_evidence_bundle",
|
||||
arguments: {},
|
||||
@@ -97,6 +101,7 @@ export async function verifyLegacyGhidraReferenceSnapshot(
|
||||
},
|
||||
}),
|
||||
],
|
||||
workflow_entries: [],
|
||||
evidence_bundle: createEvidenceBundle([evidence]),
|
||||
});
|
||||
assert.equal(
|
||||
@@ -107,7 +112,7 @@ export async function verifyLegacyGhidraReferenceSnapshot(
|
||||
const path = join(env.TMPDIR, `legacy-reference-${randomUUID()}.json`);
|
||||
await writeFile(path, serializeAnalysisSnapshot(snapshot), { flag: "wx" });
|
||||
try {
|
||||
const session = requireMcpResult(
|
||||
const session = requireMcpLifecycleResult(
|
||||
await client.callTool({ name: "binary_session", arguments: {} }),
|
||||
"binary_session",
|
||||
);
|
||||
@@ -120,7 +125,7 @@ export async function verifyLegacyGhidraReferenceSnapshot(
|
||||
},
|
||||
});
|
||||
assert.equal(reply.isError, true);
|
||||
const mcpRejection = reply.structuredContent.error;
|
||||
const mcpRejection = requireMcpToolError(reply);
|
||||
assert.equal(mcpRejection.code, "evidence_integrity_mismatch");
|
||||
assert.match(mcpRejection.details.reason, /profile_mismatch/u);
|
||||
assert.match(
|
||||
@@ -128,14 +133,14 @@ export async function verifyLegacyGhidraReferenceSnapshot(
|
||||
/without this snapshot.*fresh snapshot/u,
|
||||
);
|
||||
assert.deepEqual(
|
||||
requireMcpResult(
|
||||
requireMcpLifecycleResult(
|
||||
await client.callTool({ name: "binary_session", arguments: {} }),
|
||||
"binary_session",
|
||||
),
|
||||
session,
|
||||
"A rejected snapshot must preserve the selected session",
|
||||
);
|
||||
const retained = requireMcpResult(
|
||||
const retained = requireMcpEvidenceResult(
|
||||
await client.callTool({
|
||||
name: "analyze_function",
|
||||
arguments: { procedure },
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { analysisErrorProjectionSchema } from "../../dist/contracts/errorSchemas.js";
|
||||
import { toolContract } from "../../dist/contracts/toolContracts.js";
|
||||
|
||||
/** Read the textual JSON projection from one MCP tool call. */
|
||||
export const mcpTextValue = (result) => {
|
||||
const text = result.content?.find((item) => item.type === "text")?.text;
|
||||
@@ -7,25 +10,64 @@ export const mcpTextValue = (result) => {
|
||||
|
||||
const jsonValue = (result) => JSON.parse(mcpTextValue(result));
|
||||
|
||||
/** Return the compact result projection from one successful MCP tool call. */
|
||||
export const requireMcpResult = (result, operation) => {
|
||||
if (result.isError === true) {
|
||||
/** Read the complete canonical diagnostic from an MCP tool failure. */
|
||||
export const requireMcpToolError = (result) => {
|
||||
if (result.isError !== true) throw new Error("Expected an MCP tool failure");
|
||||
return analysisErrorProjectionSchema.parse(jsonValue(result).error);
|
||||
};
|
||||
|
||||
const requireOutput = (result, operation, contract) => {
|
||||
if (result.isError === true)
|
||||
throw new Error(`${operation} failed: ${mcpTextValue(result)}`);
|
||||
}
|
||||
const value = jsonValue(result);
|
||||
if (value === null || typeof value !== "object" || !("result" in value)) {
|
||||
throw new Error(`${operation} omitted its result projection`);
|
||||
}
|
||||
return value.result;
|
||||
if (contract.name !== operation)
|
||||
throw new Error(`${operation} received the ${contract.name} contract`);
|
||||
return contract.outputSchema.parse(jsonValue(result));
|
||||
};
|
||||
|
||||
const operationResultKey = (contract) => {
|
||||
const shape = contract.outputSchema.shape;
|
||||
if (shape === undefined)
|
||||
throw new Error(`${contract.name} has an unsupported MCP output schema`);
|
||||
const evidence = Object.hasOwn(shape, "normalized_result");
|
||||
const lifecycle = Object.hasOwn(shape, "result");
|
||||
if (evidence === lifecycle)
|
||||
throw new Error(`${contract.name} has an unsupported MCP output schema`);
|
||||
return evidence ? "normalized_result" : "result";
|
||||
};
|
||||
|
||||
/** Validate an Evidence-producing tool output and return its normalized result. */
|
||||
export const requireMcpEvidenceResult = (result, operation) => {
|
||||
const contract = toolContract(operation);
|
||||
if (operationResultKey(contract) !== "normalized_result")
|
||||
throw new Error(`${operation} requires the lifecycle result extractor`);
|
||||
return requireOutput(result, operation, contract).normalized_result;
|
||||
};
|
||||
|
||||
/** Validate a lifecycle tool output and return its operation result. */
|
||||
export const requireMcpLifecycleResult = (result, operation) => {
|
||||
const contract = toolContract(operation);
|
||||
if (operationResultKey(contract) !== "result")
|
||||
throw new Error(`${operation} requires the Evidence result extractor`);
|
||||
return requireOutput(result, operation, contract).result;
|
||||
};
|
||||
|
||||
/** Project a mixed verifier call using its exact named canonical output schema. */
|
||||
export const requireMcpOperationResult = (
|
||||
result,
|
||||
operation,
|
||||
contract = toolContract(operation),
|
||||
) => {
|
||||
const key = operationResultKey(contract);
|
||||
return requireOutput(result, operation, contract)[key];
|
||||
};
|
||||
|
||||
/** Verify provider identity directly from the tool result. */
|
||||
export const requireEvidenceProvider = (result, operation, providerId) => {
|
||||
const evidence = jsonValue(result);
|
||||
if (
|
||||
evidence?.evidence?.provider?.id !== providerId ||
|
||||
evidence?.evidence?.analysis_profile?.provider?.id !== providerId ||
|
||||
typeof evidence.evidence.analysis_profile.provider.version !== "string"
|
||||
evidence?.provider?.id !== providerId ||
|
||||
evidence?.analysis_profile?.provider?.id !== providerId ||
|
||||
typeof evidence.analysis_profile.provider.version !== "string"
|
||||
) {
|
||||
throw new Error(`${operation} omitted its concrete provider provenance`);
|
||||
}
|
||||
@@ -38,10 +80,10 @@ export const requireWorkflowEvidenceProvider = (
|
||||
expected,
|
||||
) => {
|
||||
const evidence = jsonValue(result);
|
||||
const profile = evidence?.evidence?.analysis_profile;
|
||||
const profile = evidence?.analysis_profile;
|
||||
const upstream = profile?.parameters?.upstream_analysis_profile;
|
||||
if (
|
||||
evidence?.evidence?.provider?.id !== expected.workflowProviderId ||
|
||||
evidence?.provider?.id !== expected.workflowProviderId ||
|
||||
profile?.provider?.id !== expected.workflowProviderId ||
|
||||
upstream?.provider?.id !== expected.upstreamProviderId ||
|
||||
typeof upstream.provider.version !== "string"
|
||||
|
||||
@@ -14,14 +14,6 @@ const MIGRATED_PROVIDER_ROOTS = new Set([
|
||||
"inspector",
|
||||
"javascript",
|
||||
]);
|
||||
const PROVIDER_ROOTS = new Set([
|
||||
...MIGRATED_PROVIDER_ROOTS,
|
||||
"artifacts",
|
||||
"browser",
|
||||
"dotnet",
|
||||
"native",
|
||||
]);
|
||||
|
||||
const repositoryPath = (root, path) =>
|
||||
relative(root, path).replaceAll("\\", "/");
|
||||
|
||||
@@ -88,11 +80,6 @@ const failedBoundary = (file, target) => {
|
||||
/^src\/(?:cli|main)\./u.test(target))
|
||||
)
|
||||
return "artifact-acquisition";
|
||||
if (
|
||||
PROVIDER_ROOTS.has(layer) &&
|
||||
/^src\/generatedMcpToolCatalog\.(?:js|ts)$/u.test(target)
|
||||
)
|
||||
return "provider-generated-catalog";
|
||||
if (PURE_LAYERS.has(layer)) {
|
||||
if (
|
||||
(/^src\/[^/]+\//u.test(target) && !PURE_LAYERS.has(targetLayer)) ||
|
||||
|
||||
@@ -25,6 +25,15 @@ const environment = () => ({
|
||||
...(process.env.JAVA_HOME === undefined
|
||||
? {}
|
||||
: { JAVA_HOME: process.env.JAVA_HOME }),
|
||||
...(process.env.GHIDRA_HEADLESS_MAXMEM === undefined
|
||||
? {}
|
||||
: { GHIDRA_HEADLESS_MAXMEM: process.env.GHIDRA_HEADLESS_MAXMEM }),
|
||||
...(process.env.GHIDRA_MAXMEM === undefined
|
||||
? {}
|
||||
: { GHIDRA_MAXMEM: process.env.GHIDRA_MAXMEM }),
|
||||
...(process.env._JAVA_OPTIONS === undefined
|
||||
? {}
|
||||
: { _JAVA_OPTIONS: process.env._JAVA_OPTIONS }),
|
||||
});
|
||||
|
||||
const verifyGraph = (input, target, procedure, globalAddress) => {
|
||||
@@ -179,12 +188,12 @@ async function verifyNativeValueAdapters(
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
assert.notEqual(result.isError, true, JSON.stringify(result));
|
||||
const mcpEvidence = parseEvidence(result.structuredContent?.evidence);
|
||||
const mcpEvidence = parseEvidence(result.structuredContent);
|
||||
assert.equal(mcpEvidence.operation, "trace_native_values");
|
||||
assert.equal(mcpEvidence.provider.id, "rea-workflow");
|
||||
assert.deepEqual(mcpEvidence.analysis_profile, workflowProfile);
|
||||
const mcpGraph = verifyGraph(
|
||||
result.structuredContent?.result,
|
||||
result.structuredContent?.normalized_result,
|
||||
target,
|
||||
procedure,
|
||||
globalAddress,
|
||||
@@ -196,9 +205,7 @@ async function verifyNativeValueAdapters(
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
assert.notEqual(analyzed.isError, true, JSON.stringify(analyzed));
|
||||
const functionEvidence = parseEvidence(
|
||||
analyzed.structuredContent?.evidence,
|
||||
);
|
||||
const functionEvidence = parseEvidence(analyzed.structuredContent);
|
||||
assert.equal(functionEvidence.operation, "analyze_function");
|
||||
assert.equal(functionEvidence.provider.id, "ghidra");
|
||||
const functionProfile = analysisProfileSchema.parse(
|
||||
@@ -207,26 +214,24 @@ async function verifyNativeValueAdapters(
|
||||
assert.deepEqual(functionProfile, upstreamProfile);
|
||||
assert.deepEqual(
|
||||
functionEvidence.normalized_result,
|
||||
analyzed.structuredContent?.result,
|
||||
analyzed.structuredContent?.normalized_result,
|
||||
);
|
||||
const comparison = await client.callTool(
|
||||
{
|
||||
name: "compare_functions",
|
||||
arguments: {
|
||||
left: analyzed.structuredContent.evidence,
|
||||
right: analyzed.structuredContent.evidence,
|
||||
left: analyzed.structuredContent,
|
||||
right: analyzed.structuredContent,
|
||||
},
|
||||
},
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
assert.notEqual(comparison.isError, true, JSON.stringify(comparison));
|
||||
const comparisonEvidence = parseEvidence(
|
||||
comparison.structuredContent?.evidence,
|
||||
);
|
||||
const comparisonEvidence = parseEvidence(comparison.structuredContent);
|
||||
assert.equal(comparisonEvidence.normalized_result.status, "unchanged");
|
||||
assert.deepEqual(
|
||||
comparisonEvidence.normalized_result,
|
||||
comparison.structuredContent?.result,
|
||||
comparison.structuredContent?.normalized_result,
|
||||
);
|
||||
assert.ok(
|
||||
comparisonEvidence.evidence_links.includes(functionEvidence.evidence_id),
|
||||
|
||||
@@ -24,7 +24,7 @@ export const createProductCatalog = async (root) => {
|
||||
assertSameNames(
|
||||
"Primary CLI inventory",
|
||||
cli.primary,
|
||||
sources.catalogIdentity.CLI_COMMAND_NAMES,
|
||||
sources.cliCommandNames.CLI_COMMAND_NAMES,
|
||||
);
|
||||
const tools = toolFamilyCatalog(sources);
|
||||
const providers = providerCatalog(sources);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { requireMcpToolError } from "./mcp-verifier-results.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
/** Exercise segmented-address truncation through real CLI/MCP reads and edits. */
|
||||
@@ -8,7 +9,7 @@ export async function verifyGhidraAddressBoundaries(client, entry, cli) {
|
||||
{ timeout: 240000 },
|
||||
);
|
||||
assert.notEqual(reply.isError, true, JSON.stringify(reply));
|
||||
return reply.structuredContent.result;
|
||||
return reply.structuredContent.normalized_result;
|
||||
};
|
||||
const before = await query("analyze_function", { procedure: entry });
|
||||
// The segmented parser used to discard the high 32 bits and alias this entry.
|
||||
@@ -40,13 +41,14 @@ export async function verifyGhidraAddressBoundaries(client, entry, cli) {
|
||||
{ timeout: 240000 },
|
||||
);
|
||||
assert.equal(reply.isError, true, `${name} accepted ${address}`);
|
||||
const error = reply.structuredContent?.error;
|
||||
const error = requireMcpToolError(reply);
|
||||
assert.equal(error?.code, "invalid_request", JSON.stringify(reply));
|
||||
assert.equal(error.details.operation, name);
|
||||
const issues = JSON.stringify(error.details.issues);
|
||||
assert.match(issues, /cannot be represented without truncation/u);
|
||||
assert.ok(issues.includes(address), issues);
|
||||
assert.equal(reply.structuredContent.evidence, undefined);
|
||||
// A rejected request produces no Evidence; errors carry text only.
|
||||
assert.equal(reply.structuredContent, undefined);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
/** Verify batch selector/entry separation against real source-owned Ghidra functions. */
|
||||
export async function verifyGhidraBatchIdentity(call, names) {
|
||||
const leaf = names.find((item) =>
|
||||
item.value.endsWith("rea_ghidra_inventory_leaf"),
|
||||
);
|
||||
assert.ok(leaf, "Source fixture lacks a leaf procedure");
|
||||
const listing = await call("read_function_instructions", {
|
||||
procedure: leaf.address,
|
||||
});
|
||||
const interior = listing.instructions[1]?.split(": ")[0];
|
||||
assert.ok(interior, "Source fixture lacks an interior instruction");
|
||||
const name = await call("address_name", { address: leaf.address });
|
||||
const selectors = [leaf.address, interior, leaf.value, "0x0"];
|
||||
const result = await call("batch_decompile", { addresses: selectors });
|
||||
assert.equal(result.total, 4);
|
||||
assert.equal(result.succeeded, 3);
|
||||
assert.equal(result.failed, 1);
|
||||
for (let index = 0; index < 3; index++) {
|
||||
const item = result.items[index];
|
||||
assert.equal(item.address, selectors[index]);
|
||||
assert.equal(item.status, "ok");
|
||||
assert.deepEqual(item.procedure, {
|
||||
status: "resolved",
|
||||
address: leaf.address,
|
||||
name,
|
||||
});
|
||||
assert.equal(item.pseudocode, result.items[0].pseudocode);
|
||||
}
|
||||
assert.equal(result.items[3].status, "error");
|
||||
assert.equal(result.items[3].procedure.status, "unknown");
|
||||
assert.ok(result.items[3].procedure.error.code);
|
||||
return {
|
||||
entry: leaf.address,
|
||||
interior,
|
||||
selectors_preserved: true,
|
||||
identical_resolved_code: true,
|
||||
partial_failure_retained: true,
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
requireMcpToolError,
|
||||
mcpTextValue,
|
||||
requireMcpOperationResult,
|
||||
} from "./mcp-verifier-results.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
@@ -7,13 +12,17 @@ import { setTimeout as delay } from "node:timers/promises";
|
||||
import { promisify } from "node:util";
|
||||
import Ajv from "ajv/dist/2020.js";
|
||||
import addFormats from "ajv-formats";
|
||||
|
||||
import { analysisErrorProjectionSchema } from "../../dist/contracts/errorSchemas.js";
|
||||
import { mcpTextValue, requireMcpResult } from "./mcp-verifier-results.mjs";
|
||||
|
||||
import { verifyLegacyGhidraReferenceSnapshot } from "./ghidra-reference-snapshot-e2e.mjs";
|
||||
import { verifyGhidraSnapshotLifecycle } from "./real-ghidra-snapshot-lifecycle.mjs";
|
||||
import { verifyGhidraTargetAdmission } from "./real-ghidra-target-admission.mjs";
|
||||
import { verifyGhidraLargeResults } from "./real-ghidra-large-results.mjs";
|
||||
import { verifyGhidraEntryAliases } from "./real-ghidra-entry-aliases.mjs";
|
||||
import { verifyGhidraNamespaceAnnotations } from "./real-ghidra-namespace-annotations.mjs";
|
||||
import { verifyGhidraBatchIdentity } from "./real-ghidra-batch-identity.mjs";
|
||||
import { verifyGhidraInstructionFlow } from "./real-ghidra-instruction-flow.mjs";
|
||||
|
||||
/** Probe real Ghidra location, annotation and error contracts through public adapters. */
|
||||
export async function verifyGhidraBoundaries(
|
||||
@@ -37,7 +46,7 @@ export async function verifyGhidraBoundaries(
|
||||
let rejectedCalls = 0;
|
||||
const call = async (name, args = {}) => {
|
||||
const reply = await client.callTool({ name, arguments: args }, options);
|
||||
const result = requireMcpResult(reply, name);
|
||||
const result = requireMcpOperationResult(reply, name);
|
||||
const validate = validators.get(name);
|
||||
assert.ok(validate, `${name} missing from catalog`);
|
||||
assert.ok(
|
||||
@@ -55,23 +64,19 @@ export async function verifyGhidraBoundaries(
|
||||
true,
|
||||
`${name} accepted ${JSON.stringify(args)}`,
|
||||
);
|
||||
// SDK input-schema rejections are text-only; application errors use the
|
||||
// canonical structured error projection, separately from success schemas.
|
||||
if (reply.structuredContent !== undefined) {
|
||||
analysisErrorProjectionSchema.parse(reply.structuredContent.error);
|
||||
assert.deepEqual(
|
||||
reply.structuredContent,
|
||||
JSON.parse(mcpTextValue(reply)),
|
||||
);
|
||||
}
|
||||
// SDK input rejections are prose; application errors carry canonical JSON
|
||||
// text without publishing data outside the advertised success schema.
|
||||
const error = mcpTextValue(reply).startsWith("{")
|
||||
? requireMcpToolError(reply)
|
||||
: undefined;
|
||||
assert.equal(reply.structuredContent, undefined);
|
||||
if (diagnostic !== undefined) {
|
||||
const error = reply.structuredContent?.error;
|
||||
assert.equal(error?.code, "invalid_request");
|
||||
assert.equal(error?.details?.operation, name);
|
||||
assert.match(JSON.stringify(error.details.issues), diagnostic);
|
||||
}
|
||||
rejectedCalls++;
|
||||
return reply.structuredContent?.error;
|
||||
return error;
|
||||
};
|
||||
const cli = async (command, value, flags = []) => {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
@@ -156,6 +161,8 @@ export async function verifyGhidraBoundaries(
|
||||
/Annotations require a local function entry/u,
|
||||
);
|
||||
const names = await call("list_names");
|
||||
const batchIdentity = await verifyGhidraBatchIdentity(call, names);
|
||||
const instructionFlow = await verifyGhidraInstructionFlow(call, names, cli);
|
||||
const leaf = names.find((item) =>
|
||||
item.value.endsWith("rea_ghidra_inventory_leaf"),
|
||||
);
|
||||
@@ -278,7 +285,25 @@ export async function verifyGhidraBoundaries(
|
||||
await cli("inspect-native-instruction", address.toUpperCase()),
|
||||
baseline,
|
||||
);
|
||||
const interior = `0x${(BigInt(address) + 1n).toString(16)}`;
|
||||
// x86 function entries can begin with a one-byte PUSH. Locate an observed
|
||||
// multi-byte instruction instead of assuming entry + 1 is an interior byte.
|
||||
let multiByteInstruction;
|
||||
for (const range of original.procedure.body.ranges) {
|
||||
let cursor = BigInt(range.start);
|
||||
while (cursor <= BigInt(range.end)) {
|
||||
const instruction = await call("inspect_native_instruction", {
|
||||
address: `0x${cursor.toString(16)}`,
|
||||
});
|
||||
if (instruction.status === "decoded" && instruction.length > 1) {
|
||||
multiByteInstruction = instruction;
|
||||
break;
|
||||
}
|
||||
cursor += BigInt(instruction.length ?? 1);
|
||||
}
|
||||
if (multiByteInstruction !== undefined) break;
|
||||
}
|
||||
assert.ok(multiByteInstruction, "Fixture lacks a multi-byte instruction");
|
||||
const interior = `0x${(BigInt(multiByteInstruction.address) + 1n).toString(16)}`;
|
||||
assert.equal(
|
||||
(await call("inspect_native_instruction", { address: interior })).status,
|
||||
"not-instruction-boundary",
|
||||
@@ -455,7 +480,7 @@ export async function verifyGhidraBoundaries(
|
||||
options,
|
||||
);
|
||||
assert.equal(regexFailure.isError, true);
|
||||
const regexError = regexFailure.structuredContent.error;
|
||||
const regexError = requireMcpToolError(regexFailure);
|
||||
assert.equal(regexError.code, "resource_constraint");
|
||||
assert.equal(regexError.details.operation, "search_strings");
|
||||
assert.equal(regexError.details.resource, "memory");
|
||||
@@ -477,7 +502,7 @@ export async function verifyGhidraBoundaries(
|
||||
options,
|
||||
);
|
||||
assert.equal(compileFailure.isError, true);
|
||||
const compileError = compileFailure.structuredContent.error;
|
||||
const compileError = requireMcpToolError(compileFailure);
|
||||
assert.equal(compileError.code, "resource_constraint");
|
||||
assert.equal(compileError.details.operation, "search_procedures");
|
||||
assert.match(compileError.details.reason, /stack while compiling pattern/u);
|
||||
@@ -693,6 +718,13 @@ export async function verifyGhidraBoundaries(
|
||||
entrypoint,
|
||||
env,
|
||||
});
|
||||
await verifyGhidraEntryAliases({
|
||||
call,
|
||||
reject: invalid,
|
||||
target,
|
||||
entrypoint,
|
||||
env,
|
||||
});
|
||||
await verifyGhidraLargeResults({
|
||||
call,
|
||||
reject: invalid,
|
||||
@@ -740,7 +772,10 @@ export async function verifyGhidraBoundaries(
|
||||
missing_and_nonregular_source_rejected: true,
|
||||
imported_source_identity_retained: true,
|
||||
equivalent_instruction_address_spellings: true,
|
||||
batch_procedure_identity: batchIdentity,
|
||||
instruction_flow: instructionFlow,
|
||||
qualified_annotation_name_roundtrip: true,
|
||||
imported_entry_alias_selection: true,
|
||||
oversized_result_retention_and_complete_export: true,
|
||||
long_selector_rejection_and_provider_recovery: true,
|
||||
source_immutable: true,
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
/** Verify imported entry aliases through CLI/MCP without admitting interior labels. */
|
||||
export async function verifyGhidraEntryAliases({
|
||||
call,
|
||||
reject,
|
||||
target,
|
||||
entrypoint,
|
||||
env,
|
||||
}) {
|
||||
const exec = promisify(execFile);
|
||||
const compiler = process.env.REA_CC ?? "cc";
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-ghidra-entry-aliases-"));
|
||||
const source = fileURLToPath(
|
||||
new URL("../../tests/conformance/ghidra/entry-aliases.c", import.meta.url),
|
||||
);
|
||||
const path = join(workspace, "entry-aliases");
|
||||
const prefix = process.platform === "darwin" ? "_" : "";
|
||||
try {
|
||||
await exec(compiler, ["-O0", "-g", "-fno-inline", source, "-o", path], {
|
||||
env,
|
||||
timeout: 30000,
|
||||
});
|
||||
const bytes = await readFile(path);
|
||||
await call("close_binary");
|
||||
await call("open_binary", { path, provider_id: "ghidra" });
|
||||
const names = await call("list_names");
|
||||
const alias = names.find(
|
||||
({ value }) => value === `${prefix}rea_entry_alias`,
|
||||
);
|
||||
assert.ok(alias, "Fixture entry alias was not imported");
|
||||
assert.equal(
|
||||
alias.symbol.primary,
|
||||
false,
|
||||
"The regression must exercise a secondary entry symbol",
|
||||
);
|
||||
assert.equal(alias.symbol.source, "imported");
|
||||
const canonical = (await call("list_procedures")).find(
|
||||
({ address }) => address === alias.address,
|
||||
);
|
||||
assert.ok(canonical, "Fixture alias is not at a function entry");
|
||||
const xrefsSelectors = [];
|
||||
for (const value of [
|
||||
canonical.value,
|
||||
alias.value,
|
||||
`${prefix}rea_xrefs_data`,
|
||||
`${prefix}rea_xrefs_unreferenced`,
|
||||
]) {
|
||||
const symbol = names.find((item) => item.value === value);
|
||||
assert.ok(symbol, `Missing xrefs selector ${value}`);
|
||||
const expected = await call("xrefs", { address: symbol.address });
|
||||
if (value === `${prefix}rea_xrefs_unreferenced`)
|
||||
assert.deepEqual(expected, []);
|
||||
else assert.ok(expected.length > 0, `Missing references for ${value}`);
|
||||
xrefsSelectors.push({ symbol, expected });
|
||||
}
|
||||
for (const procedure of [
|
||||
alias.value,
|
||||
"dead",
|
||||
canonical.value,
|
||||
alias.address,
|
||||
]) {
|
||||
assert.equal(
|
||||
await call("procedure_address", { procedure }),
|
||||
alias.address,
|
||||
);
|
||||
assert.equal(
|
||||
(await call("analyze_function", { procedure })).procedure.address,
|
||||
alias.address,
|
||||
);
|
||||
assert.equal(
|
||||
await call("procedure_pseudo_code", { procedure }),
|
||||
await call("procedure_pseudo_code", { procedure: alias.address }),
|
||||
);
|
||||
}
|
||||
// Leaf and qualified forms of the primary name must not duplicate a match.
|
||||
assert.equal(
|
||||
(await call("procedure_info", { procedure: alias.value })).entrypoint,
|
||||
alias.address,
|
||||
);
|
||||
const interior = names.find(
|
||||
({ value }) => value === `${prefix}rea_interior`,
|
||||
);
|
||||
assert.ok(interior);
|
||||
assert.notEqual(interior.address, alias.address);
|
||||
const failure = await reject("procedure_address", {
|
||||
procedure: interior.value,
|
||||
});
|
||||
assert.equal(failure.code, "invalid_request");
|
||||
assert.match(
|
||||
JSON.stringify(failure.details.issues),
|
||||
/Unknown Ghidra procedure name or address/u,
|
||||
);
|
||||
assert.equal(
|
||||
await call("procedure_address", { procedure: interior.address }),
|
||||
alias.address,
|
||||
"Explicit interior addresses keep their existing containment semantics",
|
||||
);
|
||||
const edited = await call("annotate_native_function", {
|
||||
procedure: alias.value,
|
||||
comment: "Selected by imported entry alias",
|
||||
});
|
||||
assert.equal(edited.annotations.address, alias.address);
|
||||
assert.equal(
|
||||
edited.annotations.comment,
|
||||
"Selected by imported entry alias",
|
||||
);
|
||||
assert.equal(
|
||||
await call("procedure_address", { procedure: alias.value }),
|
||||
alias.address,
|
||||
);
|
||||
await call("close_binary");
|
||||
const { stdout } = await exec(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"function",
|
||||
path,
|
||||
alias.value,
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
],
|
||||
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
assert.equal(
|
||||
JSON.parse(stdout).normalized_result.procedure.address,
|
||||
alias.address,
|
||||
);
|
||||
for (const { symbol, expected } of xrefsSelectors) {
|
||||
for (const selector of [
|
||||
[symbol.value],
|
||||
["--address", symbol.value],
|
||||
[symbol.address],
|
||||
]) {
|
||||
const { stdout } = await exec(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"xrefs",
|
||||
path,
|
||||
...selector,
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
],
|
||||
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const evidence = JSON.parse(stdout);
|
||||
assert.deepEqual(evidence.normalized_result, expected);
|
||||
assert.equal(evidence.parameters.address, selector.at(-1));
|
||||
}
|
||||
}
|
||||
await assert.rejects(
|
||||
exec(
|
||||
process.execPath,
|
||||
[
|
||||
entrypoint,
|
||||
"xrefs",
|
||||
path,
|
||||
"rea_missing_symbol",
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
],
|
||||
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
|
||||
),
|
||||
(error) => {
|
||||
const failure = JSON.parse(error.stdout);
|
||||
assert.equal(failure.code, "invalid_request");
|
||||
assert.equal(failure.details.operation, "xrefs");
|
||||
assert.match(
|
||||
JSON.stringify(failure.details.issues),
|
||||
/Unknown analyzed symbol name.*rea_missing_symbol/u,
|
||||
);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
assert.deepEqual(await readFile(path), bytes);
|
||||
await call("open_binary", { path: target.path, provider_id: "ghidra" });
|
||||
} finally {
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
/** Verify decoded return evidence and distinct non-return flows on owned fixtures. */
|
||||
export async function verifyGhidraInstructionFlow(call, names, cli) {
|
||||
const leaf = names.find((item) =>
|
||||
item.value.endsWith("rea_ghidra_inventory_leaf"),
|
||||
);
|
||||
assert.ok(leaf, "Source fixture lacks its returning leaf");
|
||||
const leafInstructions = await inspectFunction(call, leaf.address);
|
||||
const returned = leafInstructions.find(
|
||||
(instruction) => instruction.flow.kind === "return",
|
||||
);
|
||||
assert.ok(returned, "Returning source leaf lacks decoded RETURN evidence");
|
||||
assertReturnEvidence(returned);
|
||||
assert.equal(
|
||||
(await call("resolve_native_call_targets", { address: returned.address }))
|
||||
.status,
|
||||
"not-call",
|
||||
);
|
||||
assert.deepEqual(
|
||||
await cli("inspect-native-instruction", returned.address),
|
||||
returned,
|
||||
);
|
||||
|
||||
const controls = [
|
||||
["rea_ghidra_flow_return", "RET", "c3"],
|
||||
["rea_ghidra_flow_cleanup_return", "RET", "c21000"],
|
||||
["rea_ghidra_flow_shared_tail", "JMP", null],
|
||||
["rea_ghidra_flow_trap", "UD2", "0f0b"],
|
||||
];
|
||||
let checked = 0;
|
||||
for (const [symbol, mnemonic, bytes] of controls) {
|
||||
const procedure = names.find((item) => item.value === symbol);
|
||||
if (procedure === undefined) continue;
|
||||
const before = await call("resolve_containing_procedure", {
|
||||
address: procedure.address,
|
||||
});
|
||||
const instructions = await inspectFunction(call, procedure.address);
|
||||
const instruction = instructions.find((item) => item.mnemonic === mnemonic);
|
||||
assert.ok(instruction, `Source fixture lacks ${symbol}'s ${mnemonic}`);
|
||||
if (bytes !== null) assert.equal(instruction.bytes, bytes);
|
||||
if (mnemonic === "RET") {
|
||||
assertReturnEvidence(instruction);
|
||||
assert.equal(instruction.flow.kind, "return");
|
||||
if (bytes === "c21000")
|
||||
assert.equal(instruction.operands[0]?.raw, "0x10");
|
||||
} else {
|
||||
assert.notEqual(instruction.flow.kind, "return");
|
||||
assert.ok(
|
||||
instruction.flow.classification_evidence.some(
|
||||
(item) => item.source === "ghidra-listing-flow-type",
|
||||
),
|
||||
);
|
||||
}
|
||||
assert.deepEqual(
|
||||
await call("resolve_containing_procedure", {
|
||||
address: procedure.address,
|
||||
}),
|
||||
before,
|
||||
);
|
||||
checked++;
|
||||
}
|
||||
const unavailable = await call("inspect_native_instruction", {
|
||||
address: "0x0",
|
||||
});
|
||||
assert.equal(unavailable.status, "outside-memory");
|
||||
assert.equal(unavailable.flow.kind, "unavailable");
|
||||
assert.deepEqual(unavailable.flow.classification_evidence, []);
|
||||
return {
|
||||
return_instruction: returned.address,
|
||||
decoded_pcode_return: true,
|
||||
return_cli_mcp_parity: true,
|
||||
explicit_x86_elf_controls: checked,
|
||||
explicit_control_scope:
|
||||
checked === 0
|
||||
? "Not present on this host-native fixture format/architecture"
|
||||
: "x86-64 ELF",
|
||||
generic_unknown_preserved: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function inspectFunction(call, procedure) {
|
||||
const listing = await call("read_function_instructions", { procedure });
|
||||
const instructions = [];
|
||||
for (const line of listing.instructions) {
|
||||
const delimiter = line.indexOf(": ");
|
||||
assert.ok(delimiter > 0, `Malformed provider instruction listing: ${line}`);
|
||||
instructions.push(
|
||||
await call("inspect_native_instruction", {
|
||||
address: line.slice(0, delimiter),
|
||||
}),
|
||||
);
|
||||
}
|
||||
return instructions;
|
||||
}
|
||||
|
||||
function assertReturnEvidence(instruction) {
|
||||
assert.equal(instruction.status, "decoded");
|
||||
assert.ok(
|
||||
instruction.flow.classification_evidence.some(
|
||||
(item) =>
|
||||
item.source === "ghidra-instruction-pcode" && item.value === "RETURN",
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
!instruction.limitations.some((text) =>
|
||||
text.includes("not classified as a return"),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -64,11 +64,13 @@ export async function verifyGhidraLargeResults({
|
||||
assert.equal(expected[0], 0x5a);
|
||||
assert.equal(expected.at(-1), 0xa5);
|
||||
const before = await call("binary_session");
|
||||
// This request fits the SDK receive limit, but repeating the producer's
|
||||
// diagnostic in text and structured content cannot fit the response.
|
||||
// An error reaches the wire once, as escaped JSON text. Each quote costs
|
||||
// two request bytes but four response bytes once the echoed diagnostic is
|
||||
// escaped again as text, so this request fits the SDK receive limit while
|
||||
// the error response cannot.
|
||||
const missingSelector =
|
||||
"REA_MISSING_" +
|
||||
"x".repeat(Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 2) + 64 * 1024);
|
||||
'"'.repeat(Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 4) + 64 * 1024);
|
||||
const lookupError = await reject("procedure_address", {
|
||||
procedure: missingSelector,
|
||||
});
|
||||
@@ -87,9 +89,12 @@ export async function verifyGhidraLargeResults({
|
||||
length,
|
||||
});
|
||||
const readId = retainedReference(readError, "read_bytes");
|
||||
// Two annotation strings repeated in text and structured content must
|
||||
// exceed the receive budget even without obsolete Evidence envelopes.
|
||||
// The request contains only the two strings and remains below that budget.
|
||||
const comment =
|
||||
"REA_BIG_COMMENT:" +
|
||||
"x".repeat(Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 10));
|
||||
"x".repeat(Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 4));
|
||||
const annotationError = await reject("annotate_native_function", {
|
||||
procedure: entry.address,
|
||||
name: "rea_frame_probe",
|
||||
|
||||
@@ -161,6 +161,7 @@ export async function verifyGhidraNamespaceAnnotations({
|
||||
|
||||
async function requireNamespaceDemangler(env) {
|
||||
const inspection = inspectGhidraInstallation({
|
||||
environment: process.env,
|
||||
installDir: env.GHIDRA_INSTALL_DIR,
|
||||
...(env.JAVA_HOME === undefined ? {} : { javaHome: env.JAVA_HOME }),
|
||||
});
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import {
|
||||
requireMcpToolError,
|
||||
requireMcpOperationResult,
|
||||
} from "./mcp-verifier-results.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import { access, mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { parseAnalysisSnapshot } from "../../dist/domain/analysisSnapshot.js";
|
||||
import { parseEvidenceBundle } from "../../dist/domain/evidenceBundle.js";
|
||||
import { requireMcpResult } from "./mcp-verifier-results.mjs";
|
||||
|
||||
/** Keep mutable Ghidra observations out of immutable snapshots across target opens. */
|
||||
export async function verifyGhidraSnapshotLifecycle(
|
||||
@@ -17,7 +20,7 @@ export async function verifyGhidraSnapshotLifecycle(
|
||||
let successfulCalls = 0;
|
||||
let rejectedCalls = 0;
|
||||
const call = async (name, args = {}) => {
|
||||
const result = requireMcpResult(
|
||||
const result = requireMcpOperationResult(
|
||||
await client.callTool({ name, arguments: args }, { timeout: 180000 }),
|
||||
name,
|
||||
);
|
||||
@@ -31,7 +34,7 @@ export async function verifyGhidraSnapshotLifecycle(
|
||||
true,
|
||||
`${name} accepted a mutated snapshot binding`,
|
||||
);
|
||||
const error = reply.structuredContent?.error;
|
||||
const error = requireMcpToolError(reply);
|
||||
assert.equal(
|
||||
error?.code,
|
||||
"evidence_integrity_mismatch",
|
||||
|
||||
@@ -6,8 +6,14 @@ import { basename, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import Ajv from "ajv/dist/2020.js";
|
||||
import addFormats from "ajv-formats";
|
||||
import { mcpTextValue, requireMcpResult } from "./mcp-verifier-results.mjs";
|
||||
import {
|
||||
mcpTextValue,
|
||||
requireMcpEvidenceResult,
|
||||
requireMcpOperationResult,
|
||||
requireMcpLifecycleResult,
|
||||
} from "./mcp-verifier-results.mjs";
|
||||
import { verifyHopperCliSelectors } from "./real-hopper-cli-selectors.mjs";
|
||||
import { verifyLinuxHopperLaunchIsolation } from "./real-hopper-session-isolation.mjs";
|
||||
import { verifyHopperNavigationAndText } from "./real-hopper-navigation.mjs";
|
||||
import { verifyHopperWorkflows } from "./real-hopper-workflows.mjs";
|
||||
import {
|
||||
@@ -43,7 +49,7 @@ export async function verifyHopperBoundaryContracts(
|
||||
const rejectedOperations = new Set();
|
||||
const call = async (name, args = {}) => {
|
||||
const reply = await client.callTool({ name, arguments: args }, options);
|
||||
const result = requireMcpResult(reply, name);
|
||||
const result = requireMcpOperationResult(reply, name);
|
||||
const validate = validators.get(name);
|
||||
assert.ok(validate, `${name} is missing from tools/list`);
|
||||
assert.ok(
|
||||
@@ -300,7 +306,7 @@ export async function verifyHopperLifecycleAndCli(client, options, targets) {
|
||||
const primary = join(directory, `primary-${suffix}`);
|
||||
const secondary = join(directory, `secondary-${suffix}`);
|
||||
const call = async (name, args = {}) =>
|
||||
requireMcpResult(
|
||||
requireMcpOperationResult(
|
||||
await client.callTool({ name, arguments: args }, options),
|
||||
name,
|
||||
);
|
||||
@@ -327,6 +333,16 @@ export async function verifyHopperLifecycleAndCli(client, options, targets) {
|
||||
procedure: entry.value,
|
||||
});
|
||||
const firstDocument = await call("current_document");
|
||||
const linuxApplicationIsolation =
|
||||
process.platform === "linux"
|
||||
? await verifyLinuxHopperLaunchIsolation({
|
||||
call,
|
||||
runCli,
|
||||
dispatcher: new URL("../rea.mjs", import.meta.url).pathname,
|
||||
primary,
|
||||
secondary,
|
||||
})
|
||||
: null;
|
||||
await call("open_binary", { path: secondary });
|
||||
const secondDocument = await call("current_document");
|
||||
assert.notEqual(firstDocument, secondDocument);
|
||||
@@ -464,6 +480,7 @@ export async function verifyHopperLifecycleAndCli(client, options, targets) {
|
||||
cliNamedSelectorParity,
|
||||
callerCancellationRecovered: true,
|
||||
closedDocumentAbsent: true,
|
||||
linuxApplicationIsolation,
|
||||
};
|
||||
} finally {
|
||||
await call("close_binary");
|
||||
@@ -496,14 +513,14 @@ const verifyBatchCancellation = async (client, options, procedures) => {
|
||||
/aborted/u,
|
||||
);
|
||||
assert.ok(cancelled, "cancellation did not follow a real Hopper response");
|
||||
const recovered = requireMcpResult(
|
||||
const recovered = requireMcpEvidenceResult(
|
||||
await client.callTool({ name: "list_procedures", arguments: {} }, options),
|
||||
"list_procedures after cancellation",
|
||||
"list_procedures",
|
||||
);
|
||||
assert.deepEqual(recovered, procedures);
|
||||
const status = requireMcpResult(
|
||||
const status = requireMcpLifecycleResult(
|
||||
await client.callTool({ name: "binary_session", arguments: {} }, options),
|
||||
"binary_session after cancellation",
|
||||
"binary_session",
|
||||
);
|
||||
assert.equal(status.provider_operation_health.state, "idle");
|
||||
};
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { requireMcpResult } from "./mcp-verifier-results.mjs";
|
||||
import { requireMcpEvidenceResult } from "./mcp-verifier-results.mjs";
|
||||
|
||||
export const verifyHopperFunctionBasics = async (
|
||||
client,
|
||||
options,
|
||||
procedure,
|
||||
) => {
|
||||
const containment = requireMcpResult(
|
||||
const containment = requireMcpEvidenceResult(
|
||||
await client.callTool(
|
||||
{
|
||||
name: "resolve_containing_procedure",
|
||||
@@ -25,7 +25,7 @@ export const verifyHopperFunctionBasics = async (
|
||||
);
|
||||
}
|
||||
|
||||
const references = requireMcpResult(
|
||||
const references = requireMcpEvidenceResult(
|
||||
await client.callTool(
|
||||
{
|
||||
name: "procedure_references",
|
||||
@@ -42,7 +42,7 @@ export const verifyHopperFunctionBasics = async (
|
||||
throw new Error("procedure_references returned an invalid result");
|
||||
}
|
||||
|
||||
const instructions = requireMcpResult(
|
||||
const instructions = requireMcpEvidenceResult(
|
||||
await client.callTool(
|
||||
{
|
||||
name: "read_function_instructions",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProvider.js";
|
||||
import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProviderCapabilities.js";
|
||||
import {
|
||||
requireMcpResult,
|
||||
requireMcpEvidenceResult,
|
||||
requireEvidenceProvider,
|
||||
} from "./mcp-verifier-results.mjs";
|
||||
|
||||
@@ -13,7 +13,7 @@ export async function verifyHopperInventories(client, options) {
|
||||
{ name: operation, arguments: {} },
|
||||
options,
|
||||
);
|
||||
const inventory = requireMcpResult(called, operation);
|
||||
const inventory = requireMcpEvidenceResult(called, operation);
|
||||
requireEvidenceProvider(called, operation, HOPPER_PROVIDER_IDENTITY.id);
|
||||
assert.ok(
|
||||
Array.isArray(inventory) && inventory.length > 0,
|
||||
@@ -30,7 +30,7 @@ export async function verifyHopperInventories(client, options) {
|
||||
);
|
||||
previous = address;
|
||||
}
|
||||
const filtered = requireMcpResult(
|
||||
const filtered = requireMcpEvidenceResult(
|
||||
await client.callTool(
|
||||
{
|
||||
name: operation,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProvider.js";
|
||||
import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProviderCapabilities.js";
|
||||
|
||||
/** Verify the selected Hopper candidate and concrete provider binding. */
|
||||
export const requireHopperSelection = (status, expected) => {
|
||||
|
||||
@@ -107,7 +107,7 @@ export async function verifyRealHopperFixture({
|
||||
},
|
||||
options,
|
||||
),
|
||||
"set_comment fixture marker",
|
||||
"set_comment",
|
||||
);
|
||||
if (commentSet !== true)
|
||||
throw new Error("Hopper did not read back the fixture comment marker");
|
||||
@@ -138,7 +138,7 @@ export async function verifyRealHopperFixture({
|
||||
},
|
||||
options,
|
||||
),
|
||||
"procedure_references native calls",
|
||||
"procedure_references",
|
||||
);
|
||||
assert.deepEqual(entryDossier.unresolved_calls, observed.unresolved_calls);
|
||||
for (const call of calls) {
|
||||
@@ -266,7 +266,7 @@ export async function resolveFixtureProcedure(
|
||||
},
|
||||
options,
|
||||
),
|
||||
`search_procedures ${expectedName}`,
|
||||
"search_procedures",
|
||||
);
|
||||
const exactMatches = Array.isArray(matches)
|
||||
? matches.filter(
|
||||
@@ -301,5 +301,5 @@ export const analyzeFixtureProcedure = async (
|
||||
},
|
||||
options,
|
||||
),
|
||||
`analyze_function ${procedure.name}`,
|
||||
"analyze_function",
|
||||
);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
/** Prove competing Linux launches cannot forward documents into the active MCP owner. */
|
||||
export async function verifyLinuxHopperLaunchIsolation({
|
||||
call,
|
||||
runCli,
|
||||
dispatcher,
|
||||
primary,
|
||||
secondary,
|
||||
}) {
|
||||
const documents = await call("list_documents");
|
||||
const current = await call("current_document");
|
||||
const procedures = await call("list_procedures");
|
||||
for (const target of [primary, secondary]) {
|
||||
let failure;
|
||||
try {
|
||||
await runCli([
|
||||
dispatcher,
|
||||
"inspect",
|
||||
target,
|
||||
"--provider",
|
||||
"hopper",
|
||||
"--format",
|
||||
"json",
|
||||
]);
|
||||
} catch (error) {
|
||||
assert.equal(error.code, 1);
|
||||
failure = JSON.parse(error.stdout);
|
||||
}
|
||||
assert.ok(
|
||||
failure,
|
||||
"A competing CLI launch was allowed to reach Linux Hopper",
|
||||
);
|
||||
assert.equal(failure.code, "provider_unavailable");
|
||||
assert.match(
|
||||
failure.message,
|
||||
/Linux Hopper is already active in REA session/u,
|
||||
);
|
||||
assert.ok(failure.message.includes(target));
|
||||
assert.deepEqual(
|
||||
await call("list_documents"),
|
||||
documents,
|
||||
"A rejected CLI launch changed the owning session's documents",
|
||||
);
|
||||
assert.equal(await call("current_document"), current);
|
||||
assert.deepEqual(await call("list_procedures"), procedures);
|
||||
}
|
||||
return {
|
||||
sameTargetRejected: true,
|
||||
differentTargetRejected: true,
|
||||
ownerDocumentsPreserved: true,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { setTimeout as delay } from "node:timers/promises";
|
||||
|
||||
/** Exercise owned Windows stdin through its production Writable and native boundary. */
|
||||
export async function verifyWindowsProtocolInput(packageRoot, workspace) {
|
||||
const { WindowsOwnedProcess } = await import(
|
||||
pathToFileURL(join(packageRoot, "dist/windows/WindowsOwnedProcess.js"))
|
||||
);
|
||||
const { requireWindowsNativeAuthority } = await import(
|
||||
pathToFileURL(join(packageRoot, "dist/windows/WindowsNativeLoader.js"))
|
||||
);
|
||||
const native = requireWindowsNativeAuthority();
|
||||
assert.equal(native.inspection.protocolStdin, true);
|
||||
const launch = (script) => acquire(WindowsOwnedProcess, workspace, script);
|
||||
const echo = await verifyEcho(launch);
|
||||
const cancellation = await verifyBlockedCancellation(launch);
|
||||
await verifyNativePendingWrite(native, workspace);
|
||||
const { JadxMcpTransport } = await import(
|
||||
pathToFileURL(join(packageRoot, "dist/android/JadxMcpTransport.js"))
|
||||
);
|
||||
await verifyTransportPendingWrite(JadxMcpTransport, workspace);
|
||||
return {
|
||||
echo,
|
||||
cancellation,
|
||||
native_pending_write_closed: true,
|
||||
forged_input_handle_rejected: true,
|
||||
malformed_input_rejected: true,
|
||||
provider_transport_pending_write_cancelled: true,
|
||||
};
|
||||
}
|
||||
|
||||
function acquire(Process, workspace, script) {
|
||||
const child = new Process(
|
||||
process.execPath,
|
||||
["-e", script],
|
||||
workspace,
|
||||
process.env,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.on("data", (bytes) => {
|
||||
stdout += bytes.toString();
|
||||
});
|
||||
child.stderr.on("data", (bytes) => {
|
||||
stderr += bytes.toString();
|
||||
});
|
||||
const inputErrors = [];
|
||||
child.stdin.on("error", (cause) => inputErrors.push(cause.message));
|
||||
const closed = new Promise((resolve, reject) => {
|
||||
child.once("error", reject);
|
||||
child.once("close", (code) => resolve({ code, stdout, stderr }));
|
||||
});
|
||||
return { child, closed, inputErrors, stdout: () => stdout };
|
||||
}
|
||||
|
||||
async function verifyEcho(launch) {
|
||||
const acquired = launch(`
|
||||
const {createHash} = require('node:crypto');
|
||||
const hash = createHash('sha256'); let count = 0;
|
||||
process.stdin.on('data', bytes => { hash.update(bytes); count += bytes.length; });
|
||||
process.stdin.pause(); setTimeout(() => process.stdin.resume(), 250);
|
||||
process.stdin.on('end', () => console.log(JSON.stringify({count,sha256:hash.digest('hex')})));
|
||||
`);
|
||||
const bytes = Buffer.from(
|
||||
"caller-selected UTF8: 路径 Ω; binary:\0\n".repeat(60000),
|
||||
);
|
||||
const expected = {
|
||||
count: bytes.length,
|
||||
sha256: createHash("sha256").update(bytes).digest("hex"),
|
||||
};
|
||||
let ticks = 0;
|
||||
const timer = setInterval(() => ticks++, 20);
|
||||
try {
|
||||
let writable;
|
||||
await new Promise((resolve, reject) => {
|
||||
writable = acquired.child.stdin.write(bytes, (cause) =>
|
||||
cause ? reject(cause) : resolve(),
|
||||
);
|
||||
// The native worker owns its copy before returning to the caller.
|
||||
bytes.fill(0x5a);
|
||||
});
|
||||
assert.equal(
|
||||
writable,
|
||||
false,
|
||||
"Large protocol input did not expose backpressure",
|
||||
);
|
||||
assert.ok(ticks > 0, "Input write blocked the JS event loop");
|
||||
acquired.child.stdin.end();
|
||||
const result = await deadline(acquired.closed);
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
assert.deepEqual(JSON.parse(result.stdout), expected);
|
||||
assert.deepEqual(acquired.inputErrors, []);
|
||||
assert.equal((await acquired.child.cleanup()).cleaned, true);
|
||||
return {
|
||||
bytes: expected.count,
|
||||
sha256: expected.sha256,
|
||||
backpressure: true,
|
||||
event_loop_responsive: true,
|
||||
eof_observed: true,
|
||||
};
|
||||
} finally {
|
||||
clearInterval(timer);
|
||||
await acquired.child.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyBlockedCancellation(launch) {
|
||||
const acquired = launch(`
|
||||
const {spawn} = require('node:child_process');
|
||||
const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], {stdio:'inherit'});
|
||||
console.log('owned descendant ' + child.pid);
|
||||
setInterval(() => {}, 1000);
|
||||
`);
|
||||
try {
|
||||
for (
|
||||
let attempt = 0;
|
||||
attempt < 100 && !acquired.stdout().includes("owned descendant");
|
||||
attempt++
|
||||
)
|
||||
await delay(25);
|
||||
assert.match(acquired.stdout(), /owned descendant \d+/u);
|
||||
const write = new Promise((resolve) =>
|
||||
acquired.child.stdin.write(Buffer.alloc(2 * 1024 * 1024), (cause) =>
|
||||
resolve(cause),
|
||||
),
|
||||
);
|
||||
await delay(100);
|
||||
const cleanup = await acquired.child.cleanup();
|
||||
assert.equal(cleanup.cleaned, true, cleanup.reason);
|
||||
assert.ok(
|
||||
await deadline(write),
|
||||
"Closing the owned job reported a successful blocked write",
|
||||
);
|
||||
await deadline(acquired.closed);
|
||||
return { blocked_write_cancelled: true, descendant_job_settled: true };
|
||||
} finally {
|
||||
await acquired.child.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyNativePendingWrite(native, workspace) {
|
||||
const quote = (value) =>
|
||||
`"${value.replace(/(\\*)"/gu, '$1$1\\"').replace(/(\\+)$/u, "$1$1")}"`;
|
||||
const child = native.call("process_spawn", [
|
||||
process.execPath,
|
||||
[process.execPath, "-e", "setInterval(() => {}, 1000)"]
|
||||
.map(quote)
|
||||
.join(" "),
|
||||
workspace,
|
||||
Object.entries(process.env)
|
||||
.filter(([, value]) => value !== undefined)
|
||||
.map(([key, value]) => `${key}=${value}`),
|
||||
true,
|
||||
]);
|
||||
let rejected;
|
||||
try {
|
||||
assert.throws(
|
||||
() => native.call("process_stdin_write", [child.handle, "not a Buffer"]),
|
||||
/must be a Buffer/u,
|
||||
);
|
||||
const pending = native.call("process_stdin_write", [
|
||||
child.handle,
|
||||
Buffer.alloc(2 * 1024 * 1024),
|
||||
]);
|
||||
rejected = pending.then(
|
||||
() => false,
|
||||
(cause) => cause,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
native.call("process_stdin_write", [
|
||||
child.handle,
|
||||
Buffer.from("second"),
|
||||
]),
|
||||
/pending write/u,
|
||||
);
|
||||
assert.throws(
|
||||
() => native.call("process_stdin_write", [{}, Buffer.from("forged")]),
|
||||
/does not belong to REA/u,
|
||||
);
|
||||
} finally {
|
||||
// The worker retains its handle/buffer after the JS resource closes.
|
||||
native.call("process_close", [child.handle]);
|
||||
}
|
||||
const failure = await deadline(rejected);
|
||||
assert.ok(failure instanceof Error);
|
||||
assert.match(failure.message, /Write owned process input failed/u);
|
||||
}
|
||||
|
||||
async function verifyTransportPendingWrite(Transport, workspace) {
|
||||
const transport = new Transport({
|
||||
command: process.execPath,
|
||||
arguments: ["-e", "setInterval(() => {}, 1000)"],
|
||||
cwd: workspace,
|
||||
});
|
||||
transport.onerror = () => {};
|
||||
try {
|
||||
await transport.start();
|
||||
const pending = transport
|
||||
.send({
|
||||
jsonrpc: "2.0",
|
||||
id: "blocked",
|
||||
method: "probe",
|
||||
params: { payload: "x".repeat(2 * 1024 * 1024) },
|
||||
})
|
||||
.then(
|
||||
() => false,
|
||||
(cause) => cause,
|
||||
);
|
||||
await delay(100);
|
||||
await transport.close();
|
||||
assert.ok(
|
||||
await deadline(pending),
|
||||
"Transport cancellation accepted a blocked write",
|
||||
);
|
||||
} finally {
|
||||
await transport.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function deadline(promise) {
|
||||
let timer;
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise((_, reject) => {
|
||||
timer = setTimeout(
|
||||
() => reject(new Error("Owned input fixture exceeded its deadline")),
|
||||
10000,
|
||||
);
|
||||
}),
|
||||
]).finally(() => clearTimeout(timer));
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
"""Package the public Notes example for the static website."""
|
||||
"""Prepare downloads, the sitemap and the sharing image for the static website."""
|
||||
|
||||
from html.parser import HTMLParser
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
from urllib.parse import quote
|
||||
import xml.etree.ElementTree as ET
|
||||
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
|
||||
|
||||
|
||||
SITE_ORIGIN = "https://rea.tools"
|
||||
EXAMPLE_FILES = (
|
||||
"package.json",
|
||||
"main.js",
|
||||
@@ -15,6 +21,70 @@ EXAMPLE_FILES = (
|
||||
)
|
||||
|
||||
|
||||
class IndexPolicy(HTMLParser):
|
||||
"""Read indexing directives from the authored HTML head."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.in_head = False
|
||||
self.noindex = False
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == "head":
|
||||
self.in_head = True
|
||||
if tag == "meta" and self.in_head:
|
||||
attributes = dict(attrs)
|
||||
if (attributes.get("name") or "").lower() in ("robots", "googlebot"):
|
||||
directives = re.split(
|
||||
r"[\s,]+", (attributes.get("content") or "").lower()
|
||||
)
|
||||
self.noindex |= bool(set(directives) & {"noindex", "none"})
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == "head":
|
||||
self.in_head = False
|
||||
|
||||
|
||||
def prepare_sitemap(site):
|
||||
"""Discover HTML routes automatically, excluding pages marked noindex."""
|
||||
namespace = "http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
ET.register_namespace("", namespace)
|
||||
sitemap = ET.Element(f"{{{namespace}}}urlset")
|
||||
for path in sorted(site.rglob("*.html")):
|
||||
policy = IndexPolicy()
|
||||
policy.feed(path.read_text(encoding="utf-8"))
|
||||
if policy.noindex:
|
||||
continue
|
||||
route = path.relative_to(site).as_posix()
|
||||
if path.name == "index.html":
|
||||
route = route.removesuffix("index.html")
|
||||
entry = ET.SubElement(sitemap, f"{{{namespace}}}url")
|
||||
ET.SubElement(
|
||||
entry, f"{{{namespace}}}loc"
|
||||
).text = f"{SITE_ORIGIN}/{quote(route, safe='/')}"
|
||||
ET.indent(sitemap, space=" ")
|
||||
ET.ElementTree(sitemap).write(
|
||||
site / "sitemap.xml", encoding="utf-8", xml_declaration=True
|
||||
)
|
||||
print(f"Prepared sitemap.xml ({len(sitemap)} indexable pages).")
|
||||
|
||||
|
||||
def prepare_sharing_image(site):
|
||||
"""Rasterize the maintained SVG for clients that need a PNG preview."""
|
||||
try:
|
||||
import cairosvg
|
||||
except (ImportError, OSError) as error:
|
||||
sys.exit(
|
||||
f"Cannot prepare sharing image: {error}. Install website/requirements.txt and native Cairo; see website/README.md."
|
||||
)
|
||||
source = site / "assets/social-card.svg"
|
||||
destination = site / "assets/social-card.png"
|
||||
cairosvg.svg2png(
|
||||
url=str(source), write_to=str(destination), output_width=1200, output_height=630
|
||||
)
|
||||
print("Prepared assets/social-card.png (1200 × 630).")
|
||||
|
||||
|
||||
def prepare_notes_example(site):
|
||||
"""Create a ZIP with public source only and reproducible metadata."""
|
||||
source = site / "examples/notes-electron"
|
||||
@@ -37,4 +107,7 @@ def prepare_notes_example(site):
|
||||
|
||||
if __name__ == "__main__":
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
prepare_notes_example(root / "website/public")
|
||||
site = root / "website/public"
|
||||
prepare_sharing_image(site)
|
||||
prepare_sitemap(site)
|
||||
prepare_notes_example(site)
|
||||
|
||||
+3
-6
@@ -44,7 +44,6 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
|
||||
const {
|
||||
renderCliOutputArgumentError,
|
||||
renderEmptyFilteredCliOutput,
|
||||
sanitizeCliOutput,
|
||||
validateCliOutputArguments,
|
||||
} = await import("../dist/cliOutput.js");
|
||||
const outputArguments = validateCliOutputArguments(args);
|
||||
@@ -57,13 +56,11 @@ if (!(await compiledRuntimeExists(runtimeFiles))) {
|
||||
await createCli(process.env, resultOutput).serve(args, {
|
||||
stdout: (output) => {
|
||||
if (resultOutput?.handled) {
|
||||
if (resultOutput.failed)
|
||||
process.stderr.write(sanitizeCliOutput(output));
|
||||
if (resultOutput.failed) process.stderr.write(output);
|
||||
return;
|
||||
}
|
||||
const sanitized = sanitizeCliOutput(output);
|
||||
if (sanitized.length > 0) wroteOutput = true;
|
||||
process.stdout.write(sanitized);
|
||||
if (output.length > 0) wroteOutput = true;
|
||||
process.stdout.write(output);
|
||||
},
|
||||
});
|
||||
if (!wroteOutput && !resultOutput?.handled)
|
||||
|
||||
@@ -47,15 +47,24 @@ try {
|
||||
? `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 (plan.needsBuild || plan.artifactTasks.length > 0) {
|
||||
const dependencies = await run(process.execPath, [
|
||||
"scripts/check-dependency-install.mjs",
|
||||
]);
|
||||
if (built.code !== 0) process.exitCode = built.code;
|
||||
if (dependencies.code !== 0)
|
||||
throw new Error(
|
||||
"Test prerequisites require the locked dependency installation",
|
||||
);
|
||||
const prepared = await run(process.execPath, [
|
||||
"scripts/run-exclusive.mjs",
|
||||
"artifacts",
|
||||
process.execPath,
|
||||
resolve("node_modules/turbo/bin/turbo"),
|
||||
"run",
|
||||
...(plan.needsBuild ? ["artifacts:skills"] : []),
|
||||
...plan.artifactTasks,
|
||||
]);
|
||||
if (prepared.code !== 0) process.exitCode = prepared.code;
|
||||
}
|
||||
if (!process.exitCode && !shutdownSignal) {
|
||||
const tested = await run(process.execPath, [
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
|
||||
const [, , lockName, ...command] = process.argv;
|
||||
const lockRoot = resolve(process.cwd(), ".cache", "rea-command-locks");
|
||||
const signalExitCodes = { SIGINT: 130, SIGTERM: 143 };
|
||||
|
||||
let database;
|
||||
let child;
|
||||
let forceTimer;
|
||||
let shutdownSignal;
|
||||
|
||||
process.on("SIGINT", () => handleSignal("SIGINT"));
|
||||
process.on("SIGTERM", () => handleSignal("SIGTERM"));
|
||||
process.on("disconnect", handleDisconnect);
|
||||
process.on("message", (message) => {
|
||||
if (message === "SIGINT" || message === "SIGTERM") handleSignal(message);
|
||||
});
|
||||
|
||||
if (!process.connected) {
|
||||
// The public wrapper can disappear before ESM startup installs listeners.
|
||||
// A disconnected supervisor has no authority to start a new command.
|
||||
process.exitCode = signalExitCodes.SIGTERM;
|
||||
} else if (
|
||||
typeof lockName !== "string" ||
|
||||
!/^[a-z][a-z0-9-]*$/u.test(lockName) ||
|
||||
command.length === 0
|
||||
) {
|
||||
console.error("Usage: run-exclusive.mjs <lock-name> <command> [args...]");
|
||||
process.exitCode = 64;
|
||||
disconnectParent();
|
||||
} else {
|
||||
await run();
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
await mkdir(lockRoot, { recursive: true });
|
||||
// SQLite owns acquisition and OS-backed lock release, including process
|
||||
// death. Keep the database pathname stable; deleting it would split owners.
|
||||
database = new DatabaseSync(join(lockRoot, `${lockName}.sqlite`));
|
||||
try {
|
||||
database.exec("BEGIN EXCLUSIVE");
|
||||
} catch (cause) {
|
||||
if (isBusyError(cause))
|
||||
throw new Error(
|
||||
`Another ${lockName} command is already running. Wait for it to finish.`,
|
||||
{ cause },
|
||||
);
|
||||
throw cause;
|
||||
}
|
||||
if (shutdownSignal === undefined) {
|
||||
const result = await runCommand();
|
||||
process.exitCode = result.code ?? 1;
|
||||
}
|
||||
} catch (cause) {
|
||||
console.error(cause instanceof Error ? cause.message : String(cause));
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
database?.close();
|
||||
clearTimeout(forceTimer);
|
||||
if (shutdownSignal !== undefined)
|
||||
process.exitCode = signalExitCodes[shutdownSignal];
|
||||
disconnectParent();
|
||||
}
|
||||
}
|
||||
|
||||
function runCommand() {
|
||||
return new Promise((resolveResult, reject) => {
|
||||
child = spawn(command[0], command.slice(1), {
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
shell: process.platform === "win32",
|
||||
stdio: "inherit",
|
||||
});
|
||||
child.once("error", reject);
|
||||
child.once("close", (code) => {
|
||||
// During POSIX cancellation the group leader must stay alive, retaining
|
||||
// both its group identity and SQLite ownership until the entire group is
|
||||
// killed. A completed command can still have uncooperative descendants.
|
||||
if (shutdownSignal === undefined || process.platform === "win32")
|
||||
resolveResult({ code });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function disconnectParent() {
|
||||
process.off("disconnect", handleDisconnect);
|
||||
if (process.connected) process.disconnect();
|
||||
}
|
||||
|
||||
function handleDisconnect() {
|
||||
handleSignal("SIGTERM");
|
||||
}
|
||||
|
||||
function handleSignal(signal) {
|
||||
if (shutdownSignal !== undefined) return;
|
||||
shutdownSignal = signal;
|
||||
if (child === undefined || process.platform === "win32") return;
|
||||
// Establish cancellation before signalling the group. This supervisor is
|
||||
// its live leader, so the group identifier cannot be reused during cleanup.
|
||||
process.kill(-process.pid, signal);
|
||||
forceTimer = setTimeout(() => process.kill(-process.pid, "SIGKILL"), 1_000);
|
||||
}
|
||||
|
||||
function isBusyError(cause) {
|
||||
return (
|
||||
typeof cause === "object" &&
|
||||
cause !== null &&
|
||||
"code" in cause &&
|
||||
cause.code === "ERR_SQLITE_ERROR" &&
|
||||
"errcode" in cause &&
|
||||
(cause.errcode === 5 || cause.errcode === 6)
|
||||
);
|
||||
}
|
||||
+31
-146
@@ -1,155 +1,40 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const [, , lockName, ...command] = process.argv;
|
||||
const lockRoot = resolve(process.cwd(), ".cache", "rea-command-locks");
|
||||
const lockPath = join(lockRoot, `${lockName ?? "invalid"}.lock`);
|
||||
const ownerPath = join(lockPath, "owner.json");
|
||||
const signalExitCodes = { SIGINT: 130, SIGTERM: 143 };
|
||||
|
||||
let lockHeld = false;
|
||||
let child;
|
||||
let forceTimer;
|
||||
let shutdownSignal;
|
||||
const supervisor = spawn(
|
||||
process.execPath,
|
||||
[
|
||||
fileURLToPath(new URL("./run-exclusive-command.mjs", import.meta.url)),
|
||||
...process.argv.slice(2),
|
||||
],
|
||||
{
|
||||
detached: process.platform !== "win32",
|
||||
stdio: ["inherit", "inherit", "inherit", "ipc"],
|
||||
},
|
||||
);
|
||||
|
||||
process.on("SIGINT", () => handleSignal("SIGINT"));
|
||||
process.on("SIGTERM", () => handleSignal("SIGTERM"));
|
||||
|
||||
if (
|
||||
typeof lockName !== "string" ||
|
||||
!/^[a-z][a-z0-9-]*$/u.test(lockName) ||
|
||||
command.length === 0
|
||||
) {
|
||||
console.error("Usage: run-exclusive.mjs <lock-name> <command> [args...]");
|
||||
process.exitCode = 64;
|
||||
} else {
|
||||
await run();
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
await acquireLock();
|
||||
const result = await runCommand();
|
||||
process.exitCode =
|
||||
shutdownSignal === undefined
|
||||
? (result.code ?? 1)
|
||||
: signalExitCodes[shutdownSignal];
|
||||
} catch (cause) {
|
||||
console.error(cause instanceof Error ? cause.message : String(cause));
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
await releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
async function acquireLock() {
|
||||
await mkdir(lockRoot, { recursive: true });
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
try {
|
||||
await mkdir(lockPath);
|
||||
await writeFile(
|
||||
ownerPath,
|
||||
JSON.stringify({
|
||||
pid: process.pid,
|
||||
command: command.join(" "),
|
||||
started_at: new Date().toISOString(),
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
lockHeld = true;
|
||||
return;
|
||||
} catch (cause) {
|
||||
if (!isAlreadyExistsError(cause)) throw cause;
|
||||
const owner = await readOwner();
|
||||
if (owner === null || isProcessAlive(owner.pid))
|
||||
throw new Error(
|
||||
`Another ${lockName} command is already running (pid ${String(owner?.pid ?? "unknown")}). Wait for it to finish or inspect ${lockPath}.`,
|
||||
);
|
||||
await rm(lockPath, { force: true, recursive: true });
|
||||
}
|
||||
}
|
||||
throw new Error(`Could not acquire the ${lockName} command lock`);
|
||||
}
|
||||
|
||||
async function releaseLock() {
|
||||
if (!lockHeld) return;
|
||||
lockHeld = false;
|
||||
try {
|
||||
const owner = await readOwner();
|
||||
if (owner?.pid === process.pid)
|
||||
await rm(lockPath, { force: true, recursive: true });
|
||||
} catch (cause) {
|
||||
console.error(
|
||||
`Could not remove the ${lockName} command lock: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function runCommand() {
|
||||
return new Promise((resolveResult, reject) => {
|
||||
child = spawn(command[0], command.slice(1), {
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
shell: process.platform === "win32",
|
||||
stdio: "inherit",
|
||||
});
|
||||
child.once("error", reject);
|
||||
child.once("close", (code) => {
|
||||
clearTimeout(forceTimer);
|
||||
resolveResult({ code });
|
||||
});
|
||||
for (const signal of Object.keys(signalExitCodes))
|
||||
process.on(signal, () => {
|
||||
if (shutdownSignal !== undefined) return;
|
||||
shutdownSignal = signal;
|
||||
if (supervisor.connected)
|
||||
supervisor.send(signal, (cause) => {
|
||||
if (cause !== null)
|
||||
console.error(`Could not forward cancellation: ${cause.message}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleSignal(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?.();
|
||||
}
|
||||
}
|
||||
|
||||
async function readOwner() {
|
||||
try {
|
||||
const parsed = JSON.parse(await readFile(ownerPath, "utf8"));
|
||||
const pid =
|
||||
typeof parsed === "object" && parsed !== null
|
||||
? Reflect.get(parsed, "pid")
|
||||
: undefined;
|
||||
if (typeof pid !== "number" || !Number.isInteger(pid)) return null;
|
||||
return { pid };
|
||||
} catch (cause) {
|
||||
if (isMissingFileError(cause)) return null;
|
||||
throw cause;
|
||||
}
|
||||
}
|
||||
|
||||
function isProcessAlive(pid) {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch (cause) {
|
||||
return !isMissingProcessError(cause);
|
||||
}
|
||||
}
|
||||
|
||||
function isAlreadyExistsError(cause) {
|
||||
return isNodeError(cause) && cause.code === "EEXIST";
|
||||
}
|
||||
|
||||
function isMissingFileError(cause) {
|
||||
return isNodeError(cause) && cause.code === "ENOENT";
|
||||
}
|
||||
|
||||
function isMissingProcessError(cause) {
|
||||
return isNodeError(cause) && cause.code === "ESRCH";
|
||||
}
|
||||
|
||||
function isNodeError(cause) {
|
||||
return typeof cause === "object" && cause !== null && "code" in cause;
|
||||
}
|
||||
supervisor.once("error", (cause) => {
|
||||
console.error(cause.message);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
supervisor.once("close", (code) => {
|
||||
process.exitCode =
|
||||
shutdownSignal === undefined
|
||||
? (code ?? 1)
|
||||
: signalExitCodes[shutdownSignal];
|
||||
});
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Exercise automatic route discovery and mistakes that break search metadata."""
|
||||
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def load_script(name):
|
||||
"""Load a repository script without executing its command-line entrypoint."""
|
||||
spec = importlib.util.spec_from_file_location(name, ROOT / "scripts" / f"{name}.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
PREPARE = load_script("prepare-website")
|
||||
VERIFY = load_script("verify-website")
|
||||
|
||||
|
||||
class WebsiteSearchChecks(unittest.TestCase):
|
||||
"""Use authored pages and temporary fixtures to check the publishing boundary."""
|
||||
|
||||
def test_sitemap_tracks_added_moved_removed_and_nonindexable_pages(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
site = Path(directory)
|
||||
|
||||
def page(name, head="", body=""):
|
||||
path = site / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(f"<html><head>{head}</head><body>{body}</body></html>")
|
||||
return path
|
||||
|
||||
page("index.html")
|
||||
nested = page("guides/new/index.html")
|
||||
page(
|
||||
"examples/fixture/index.html",
|
||||
'<meta name="robots" content="NOINDEX, follow">',
|
||||
)
|
||||
page("draft.html", '<meta name="googlebot" content="none">')
|
||||
page("code & notes.html")
|
||||
page("visible/index.html", body='<meta name="robots" content="noindex">')
|
||||
(site / "asset.json").write_text("{}")
|
||||
|
||||
def urls():
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
PREPARE.prepare_sitemap(site)
|
||||
sitemap = ET.parse(site / "sitemap.xml")
|
||||
return {node.text for node in sitemap.findall(".//{*}loc")}
|
||||
|
||||
self.assertEqual(
|
||||
urls(),
|
||||
{
|
||||
"https://rea.tools/",
|
||||
"https://rea.tools/guides/new/",
|
||||
"https://rea.tools/code%20%26%20notes.html",
|
||||
"https://rea.tools/visible/",
|
||||
},
|
||||
)
|
||||
nested.rename(site / "moved.html")
|
||||
self.assertIn("https://rea.tools/moved.html", urls())
|
||||
self.assertNotIn("https://rea.tools/guides/new/", urls())
|
||||
(site / "moved.html").unlink()
|
||||
self.assertNotIn("https://rea.tools/moved.html", urls())
|
||||
|
||||
def test_verifier_rejects_wrong_canonicals_stale_sitemaps_and_missing_previews(
|
||||
self,
|
||||
):
|
||||
cases = (
|
||||
(
|
||||
"index.html",
|
||||
'rel="canonical" href="https://rea.tools/"',
|
||||
'rel="canonical" href="https://morluto.github.io/rea/"',
|
||||
"head canonical",
|
||||
),
|
||||
(
|
||||
"showcase/aegis/index.html",
|
||||
'rel="canonical" href="https://rea.tools/showcase/aegis/"',
|
||||
'rel="canonical" href="https://rea.tools/"',
|
||||
"head canonical",
|
||||
),
|
||||
(
|
||||
"examples/notes-electron/index.html",
|
||||
'content="noindex"',
|
||||
'content="index"',
|
||||
"must have a head noindex",
|
||||
),
|
||||
(
|
||||
"sitemap.xml",
|
||||
"https://rea.tools/faq/",
|
||||
"https://rea.tools/deleted/",
|
||||
"each indexable page once",
|
||||
),
|
||||
(
|
||||
"faq/index.html",
|
||||
'name="twitter:card" content="summary_large_image"',
|
||||
'name="twitter:card" content="summary"',
|
||||
"twitter:card",
|
||||
),
|
||||
)
|
||||
for name, old, new, message in cases:
|
||||
with self.subTest(name=name), tempfile.TemporaryDirectory() as directory:
|
||||
site = Path(directory) / "public"
|
||||
shutil.copytree(ROOT / "website/public", site)
|
||||
path = site / name
|
||||
source = path.read_text()
|
||||
self.assertIn(old, source)
|
||||
path.write_text(source.replace(old, new, 1))
|
||||
self.assertTrue(
|
||||
any(message in error for error in VERIFY.check_site(site)[3])
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
site = Path(directory) / "public"
|
||||
shutil.copytree(ROOT / "website/public", site)
|
||||
(site / "assets/social-card.png").unlink()
|
||||
self.assertIn(
|
||||
"Social preview PNG is missing; run scripts/prepare-website.py",
|
||||
VERIFY.check_site(site)[3],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -5,9 +5,16 @@ import { dirname, join, resolve } from "node:path";
|
||||
import { createInterface } from "node:readline";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { createPackage } from "@electron/asar";
|
||||
|
||||
import { evaluateCodexEvents } from "../dist/evaluation/CodexAgentEval.js";
|
||||
import {
|
||||
createAgentEvaluationFixtures,
|
||||
agentFixtureClaims,
|
||||
agentFixtureAnswerInstructions,
|
||||
} from "../dist/evaluation/AgentEvaluationFixtures.js";
|
||||
import {
|
||||
agentEvaluationPassed,
|
||||
summarizeFactualCorrectness,
|
||||
} from "../dist/evaluation/AgentEvaluationReport.js";
|
||||
import { MCP_STARTUP_POLICY } from "../dist/mcpStartupPolicy.js";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
|
||||
@@ -91,6 +98,10 @@ try {
|
||||
expectedFirstTool: "inspect_managed_artifact",
|
||||
requiresEvidence: true,
|
||||
requiredAnswerTermGroups: [["profile"], ["main", "entry point"]],
|
||||
requiredToolSubsequence: [
|
||||
"inspect_managed_artifact",
|
||||
"inspect_managed_members",
|
||||
],
|
||||
prompt: `Explain the public managed types and entry point in ${targets.managed}. Use shipped-artifact evidence and distinguish unavailable behavior from observed metadata.`,
|
||||
},
|
||||
{
|
||||
@@ -134,7 +145,13 @@ try {
|
||||
const results = [];
|
||||
for (const scenario of scenarios) {
|
||||
process.stderr.write(`Running Codex agent evaluation: ${scenario.id}\n`);
|
||||
const execution = await runCodex(scenario.prompt);
|
||||
const fixtureClaims = agentFixtureClaims(scenario.id, targets);
|
||||
const answerInstructions = agentFixtureAnswerInstructions(scenario.id);
|
||||
const execution = await runCodex(
|
||||
answerInstructions.length === 0
|
||||
? scenario.prompt
|
||||
: `${scenario.prompt}\n\n${answerInstructions}`,
|
||||
);
|
||||
const transcriptDirectory = process.env.REA_AGENT_EVAL_TRANSCRIPT_DIR;
|
||||
if (transcriptDirectory !== undefined) {
|
||||
const transcriptPath = resolve(
|
||||
@@ -155,14 +172,16 @@ try {
|
||||
requiredAnswerTermGroups: scenario.requiredAnswerTermGroups,
|
||||
requiredToolSubsequence: scenario.requiredToolSubsequence,
|
||||
forbidInputValidationFailures: true,
|
||||
...(fixtureClaims === undefined ? {} : { fixtureClaims }),
|
||||
},
|
||||
);
|
||||
const result = {
|
||||
id: scenario.id,
|
||||
expectedFirstTool: scenario.expectedFirstTool,
|
||||
requiresEvidence: scenario.requiresEvidence,
|
||||
qualityCriteria: scenario.requiredAnswerTermGroups,
|
||||
requiredAnswerTermGroups: scenario.requiredAnswerTermGroups,
|
||||
requiredToolSubsequence: scenario.requiredToolSubsequence ?? [],
|
||||
configuredClaimIds: fixtureClaims?.map(({ id }) => id) ?? [],
|
||||
exitCode: execution.exitCode,
|
||||
stderr: execution.stderr,
|
||||
...metrics,
|
||||
@@ -173,12 +192,16 @@ try {
|
||||
);
|
||||
}
|
||||
|
||||
const factualCorrectness = summarizeFactualCorrectness(results);
|
||||
const summary = {
|
||||
schemaVersion: 3,
|
||||
evaluationScope: "routing_workflow_and_configured_fixture_claims",
|
||||
factualCorrectness,
|
||||
verifier_run: await completeVerifierRun(verifierRun),
|
||||
codex,
|
||||
codexVersion,
|
||||
model: optionalModel ?? null,
|
||||
scenarios: results.map(({ finalMessage: _message, ...result }) => result),
|
||||
scenarios: results,
|
||||
totals: {
|
||||
scenarios: results.length,
|
||||
naturalUse: results.filter(({ naturalUse }) => naturalUse).length,
|
||||
@@ -205,12 +228,15 @@ try {
|
||||
(total, { cachedInputTokens }) => total + cachedInputTokens,
|
||||
0,
|
||||
),
|
||||
completionQuality: results.filter(({ completionQuality }) =>
|
||||
Boolean(completionQuality),
|
||||
answerHeuristicsMet: results.filter(({ answerHeuristicsMet }) =>
|
||||
Boolean(answerHeuristicsMet),
|
||||
).length,
|
||||
authorityHonesty: results.filter(({ authorityHonesty }) =>
|
||||
Boolean(authorityHonesty),
|
||||
epistemicCuePresent: results.filter(({ epistemicCuePresent }) =>
|
||||
Boolean(epistemicCuePresent),
|
||||
).length,
|
||||
factualScenariosPassed: factualCorrectness.counts.passed,
|
||||
factualScenariosFailed: factualCorrectness.counts.failed,
|
||||
factualScenariosNotAssessed: factualCorrectness.counts.notAssessed,
|
||||
},
|
||||
};
|
||||
const encoded = `${JSON.stringify(summary, null, 2)}\n`;
|
||||
@@ -221,31 +247,10 @@ try {
|
||||
await writeFile(resolve(reportPath), encoded);
|
||||
}
|
||||
|
||||
const failed = results.filter(
|
||||
({
|
||||
exitCode,
|
||||
naturalUse,
|
||||
correctFirstTool,
|
||||
repeatedCallCount,
|
||||
inputValidationFailureCount,
|
||||
requiredToolSubsequenceMet,
|
||||
inputTokens,
|
||||
completionQuality,
|
||||
authorityHonesty,
|
||||
}) =>
|
||||
exitCode !== 0 ||
|
||||
!naturalUse ||
|
||||
!correctFirstTool ||
|
||||
repeatedCallCount !== 0 ||
|
||||
inputValidationFailureCount !== 0 ||
|
||||
!requiredToolSubsequenceMet ||
|
||||
inputTokens <= 0 ||
|
||||
!completionQuality ||
|
||||
!authorityHonesty,
|
||||
);
|
||||
const failed = results.filter((result) => !agentEvaluationPassed(result));
|
||||
if (failed.length > 0)
|
||||
throw new Error(
|
||||
`Codex agent release evaluation failed: ${failed.map(({ id }) => id).join(", ")}`,
|
||||
`Agent routing/workflow/answer checks failed: ${failed.map(({ id }) => id).join(", ")}`,
|
||||
);
|
||||
} finally {
|
||||
if (process.env.REA_AGENT_EVAL_KEEP_FIXTURES !== "true")
|
||||
@@ -253,45 +258,7 @@ try {
|
||||
}
|
||||
|
||||
async function createTargets(root, includeManaged) {
|
||||
const javascript = join(root, "desktop-app");
|
||||
await mkdir(join(javascript, "renderer"), { recursive: true });
|
||||
await Promise.all([
|
||||
writeFile(
|
||||
join(javascript, "package.json"),
|
||||
`${JSON.stringify({ name: "desktop-fixture", version: "1.0.0", main: "main.js" }, null, 2)}\n`,
|
||||
),
|
||||
writeFile(
|
||||
join(javascript, "main.js"),
|
||||
'const { BrowserWindow, ipcMain } = require("electron");\nnew BrowserWindow({ webPreferences: { preload: require("node:path").join(__dirname, "preload.js"), contextIsolation: true, sandbox: true } });\nipcMain.handle("profile:read", (_event, id) => ({ id }));\n',
|
||||
),
|
||||
writeFile(
|
||||
join(javascript, "preload.js"),
|
||||
'const { contextBridge, ipcRenderer } = require("electron");\ncontextBridge.exposeInMainWorld("profileApi", { read: (id) => ipcRenderer.invoke("profile:read", id) });\n',
|
||||
),
|
||||
writeFile(
|
||||
join(javascript, "renderer/app.js"),
|
||||
'globalThis.profileApi.read("fixture");\n',
|
||||
),
|
||||
]);
|
||||
const javascriptAsar = join(root, "desktop-app.asar");
|
||||
await createPackage(javascript, javascriptAsar);
|
||||
|
||||
const javascriptShapeLeft = join(root, "parser-v1");
|
||||
const javascriptShapeRight = join(root, "parser-v2");
|
||||
await Promise.all([
|
||||
mkdir(javascriptShapeLeft, { recursive: true }),
|
||||
mkdir(javascriptShapeRight, { recursive: true }),
|
||||
]);
|
||||
await Promise.all([
|
||||
writeFile(
|
||||
join(javascriptShapeLeft, "parser.mjs"),
|
||||
'export default function parse() { return { heading: "Title" }; }\n',
|
||||
),
|
||||
writeFile(
|
||||
join(javascriptShapeRight, "parser.mjs"),
|
||||
'export default function parse() { return { heading: { text: "Title", level: 1 } }; }\n',
|
||||
),
|
||||
]);
|
||||
const fixtures = await createAgentEvaluationFixtures(root, repositoryRoot);
|
||||
|
||||
const managedOutput = join(root, "managed-output");
|
||||
if (includeManaged) {
|
||||
@@ -324,9 +291,7 @@ async function createTargets(root, includeManaged) {
|
||||
}
|
||||
return {
|
||||
native: "/bin/true",
|
||||
javascript: javascriptAsar,
|
||||
javascriptShapeLeft,
|
||||
javascriptShapeRight,
|
||||
...fixtures,
|
||||
managed: join(managedOutput, "AgentEval.dll"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1254,8 +1254,7 @@ async function inspect(
|
||||
return value.error;
|
||||
}
|
||||
assert.notEqual(response.isError, true, mcpTextValue(response));
|
||||
envelope = value.evidence;
|
||||
assert.deepEqual(value.result, envelope.normalized_result);
|
||||
envelope = value;
|
||||
} else {
|
||||
let response;
|
||||
try {
|
||||
|
||||
@@ -118,7 +118,7 @@ try {
|
||||
undefined,
|
||||
`Unexpected public failure: ${JSON.stringify(value)}`,
|
||||
);
|
||||
const evidence = parseEvidence(mode === "cli" ? value : value.evidence);
|
||||
const evidence = parseEvidence(value);
|
||||
const report = evidence.normalized_result;
|
||||
assert.equal(report.diagnostics.truncated, false);
|
||||
assert.equal(evidence.provider.version, "evmole@0.9.3");
|
||||
@@ -157,7 +157,7 @@ try {
|
||||
})),
|
||||
evidence.raw_result.functions ?? [],
|
||||
);
|
||||
if (mode === "mcp") assert.deepEqual(value.result, report);
|
||||
if (mode === "mcp") assert.deepEqual(value.normalized_result, report);
|
||||
return report;
|
||||
};
|
||||
const ownershipProbe = join(root.path, "ownership-prerequisite.hex");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { requireMcpToolError } from "./lib/mcp-verifier-results.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
@@ -232,12 +233,12 @@ const checkHardenedRuntime = async (directory, binary) => {
|
||||
{ timeout: CLI_TIMEOUT_MS },
|
||||
);
|
||||
assert.equal(deniedMcp.isError, true, JSON.stringify(deniedMcp));
|
||||
assert.equal(deniedMcp.structuredContent.error.code, denied.code);
|
||||
assert.equal(requireMcpToolError(deniedMcp).code, denied.code);
|
||||
assert.match(
|
||||
deniedMcp.structuredContent.error.details.reason,
|
||||
requireMcpToolError(deniedMcp).details.reason,
|
||||
/^debugger-attach-denied: /u,
|
||||
);
|
||||
assert.ok(deniedMcp.structuredContent.error.details.partial_observation);
|
||||
assert.ok(requireMcpToolError(deniedMcp).details.partial_observation);
|
||||
});
|
||||
const allowed = join(directory, "debuggable");
|
||||
const entitlements = join(directory, "get-task-allow.plist");
|
||||
|
||||
@@ -118,9 +118,14 @@ try {
|
||||
);
|
||||
const target = await parseBinaryTarget(executable);
|
||||
if (!target.ok) throw target.error;
|
||||
const observation = await observeNativeUi(target.value, "observe_native_ui", {
|
||||
...scope,
|
||||
});
|
||||
const observation = await observeNativeUi(
|
||||
target.value,
|
||||
"observe_native_ui",
|
||||
{
|
||||
...scope,
|
||||
},
|
||||
{ environment: process.env },
|
||||
);
|
||||
if (
|
||||
!observation.ok &&
|
||||
!/accessibility-denied|screen-recording-denied/u.test(
|
||||
@@ -150,6 +155,7 @@ try {
|
||||
{ kind: "wait", milliseconds: 200 },
|
||||
],
|
||||
},
|
||||
{ environment: process.env },
|
||||
);
|
||||
if (!scenario.ok) throw scenario.error;
|
||||
if (
|
||||
@@ -234,6 +240,7 @@ try {
|
||||
{ ...target.value, sha256: "0".repeat(64) },
|
||||
"observe_native_ui",
|
||||
scope,
|
||||
{ environment: process.env },
|
||||
);
|
||||
if (mismatch.ok || !mismatch.error.message.includes("target-mismatch"))
|
||||
throw new Error("Real helper did not reject changed target bytes");
|
||||
|
||||
@@ -8,13 +8,17 @@ export async function verifyPackageCapabilitiesAndSearch({ cli, environment }) {
|
||||
if (!Array.isArray(capabilities.capabilities))
|
||||
throw new Error("packaged capabilities CLI failed");
|
||||
const providers = json(await run(cli, ["providers", "--json"], environment));
|
||||
const candidates = providers.analysis_provider_candidates;
|
||||
if (
|
||||
!Array.isArray(providers.providers) ||
|
||||
providers.providers.some(({ id }) => typeof id !== "string") ||
|
||||
!Array.isArray(candidates) ||
|
||||
candidates.some(
|
||||
(candidate) =>
|
||||
typeof candidate?.provider?.id !== "string" ||
|
||||
candidate.selected !== false,
|
||||
) ||
|
||||
providers.analysis_provider_binding !== null ||
|
||||
providers.analysis_provider_candidates?.find(
|
||||
({ provider }) => provider?.id === "hopper",
|
||||
)?.target_support?.status !== "unknown"
|
||||
candidates.find(({ provider }) => provider?.id === "hopper")?.target_support
|
||||
?.status !== "unknown"
|
||||
)
|
||||
throw new Error("packaged providers CLI failed");
|
||||
if (process.platform !== "linux") {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { requireMcpToolError } from "./lib/mcp-verifier-results.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import { join } from "node:path";
|
||||
import { execFile } from "node:child_process";
|
||||
@@ -50,7 +51,7 @@ const verifyMcpUnknownProvider = async (client, mcpOptions) => {
|
||||
);
|
||||
if (
|
||||
unknownProvider.isError !== true ||
|
||||
unknownProvider.structuredContent?.error?.details?.selection_reason !==
|
||||
requireMcpToolError(unknownProvider)?.details?.selection_reason !==
|
||||
"unknown_provider"
|
||||
)
|
||||
throw new Error("packaged MCP accepted an unknown analysis provider");
|
||||
@@ -96,7 +97,7 @@ const verifyMcpNonLinuxCurrentDocument = async (
|
||||
mcpOptions,
|
||||
current,
|
||||
) => {
|
||||
if (json(prompts.mcpText(current)).result !== "fixture")
|
||||
if (json(prompts.mcpText(current)).normalized_result !== "fixture")
|
||||
throw new Error("packaged MCP bridge call failed");
|
||||
const batch = await client.callTool(
|
||||
{
|
||||
@@ -105,7 +106,7 @@ const verifyMcpNonLinuxCurrentDocument = async (
|
||||
},
|
||||
mcpOptions,
|
||||
);
|
||||
const batchResult = json(prompts.mcpText(batch)).result;
|
||||
const batchResult = json(prompts.mcpText(batch)).normalized_result;
|
||||
if (
|
||||
batch.isError === true ||
|
||||
batchResult?.total !== 1 ||
|
||||
@@ -175,10 +176,10 @@ const verifyMcpInlineArtifactEvidence = async (
|
||||
mcpOptions,
|
||||
);
|
||||
assert.notEqual(inspected.isError, true, JSON.stringify(inspected));
|
||||
const source = parseEvidence(inspected.structuredContent?.evidence);
|
||||
const source = parseEvidence(inspected.structuredContent);
|
||||
assert.deepEqual(
|
||||
source.normalized_result,
|
||||
inspected.structuredContent?.result,
|
||||
inspected.structuredContent?.normalized_result,
|
||||
);
|
||||
assert.deepEqual(
|
||||
json(prompts.mcpText(inspected)),
|
||||
@@ -188,17 +189,17 @@ const verifyMcpInlineArtifactEvidence = async (
|
||||
{
|
||||
name: "compare_artifacts",
|
||||
arguments: {
|
||||
left: inspected.structuredContent.evidence,
|
||||
right: inspected.structuredContent.evidence,
|
||||
left: inspected.structuredContent,
|
||||
right: inspected.structuredContent,
|
||||
},
|
||||
},
|
||||
mcpOptions,
|
||||
);
|
||||
assert.notEqual(compared.isError, true, JSON.stringify(compared));
|
||||
const comparison = parseEvidence(compared.structuredContent?.evidence);
|
||||
const comparison = parseEvidence(compared.structuredContent);
|
||||
assert.deepEqual(
|
||||
comparison.normalized_result,
|
||||
compared.structuredContent?.result,
|
||||
compared.structuredContent?.normalized_result,
|
||||
);
|
||||
assert.ok(comparison.evidence_links.includes(source.evidence_id));
|
||||
const bundle = await client.callTool(
|
||||
@@ -219,12 +220,12 @@ const verifyMcpInlineArtifactEvidence = async (
|
||||
mcpOptions,
|
||||
);
|
||||
assert.notEqual(extracted.isError, true, JSON.stringify(extracted));
|
||||
const extraction = parseEvidence(extracted.structuredContent?.evidence);
|
||||
const extraction = parseEvidence(extracted.structuredContent);
|
||||
assert.deepEqual(
|
||||
extraction.normalized_result,
|
||||
extracted.structuredContent?.result,
|
||||
extracted.structuredContent?.normalized_result,
|
||||
);
|
||||
const materialized = extracted.structuredContent?.result;
|
||||
const materialized = extracted.structuredContent?.normalized_result;
|
||||
assert.equal(typeof materialized?.output_root, "string");
|
||||
outputRoot = materialized.output_root;
|
||||
assert.equal(materialized.containment_verified, true);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user