Compare commits

..
Author SHA1 Message Date
Kaoru0822andClaude Opus 5.5 0c69564297 fix(electron): leave the preload unknown after a later dynamic override
A BrowserWindow webPreferences literal kept an earlier literal preload
after a later preload with a non-literal value, a spread, a computed key
or an accessor could replace it, so the graph associated the window with
the overwritten script. Derive the effective preload from the last
definition of the property, as Electron receives it.

Closes #1237

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:55:33 +00:00
550 changed files with 1745 additions and 5629 deletions
+5 -17
View File
@@ -5,7 +5,8 @@ on:
branches: [main, "release/*"]
paths:
- "website/**"
- "scripts/*website*.py"
- "scripts/verify-website.py"
- "scripts/prepare-website.py"
- ".github/workflows/**"
workflow_dispatch:
@@ -20,24 +21,11 @@ jobs:
check:
name: Website checks
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 5
steps:
- name: Check out the repository
uses: actions/checkout@v4
- 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
- name: Prepare the example download
run: python3 scripts/prepare-website.py
- name: Check website links, metadata and publishing configuration
- name: Check website links and publishing configuration
run: python3 scripts/verify-website.py
- name: Check sitemap discovery and metadata regressions
run: python3 scripts/test-website.py
+3 -14
View File
@@ -23,7 +23,7 @@ jobs:
prepare:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 5
permissions:
contents: read
pages: read
@@ -48,20 +48,9 @@ jobs:
fi
- name: Check out the repository
uses: actions/checkout@v4
- 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
- name: Prepare the example download
run: python3 scripts/prepare-website.py
- name: Check website links, metadata and publishing configuration
- name: Check website links and publishing configuration
run: python3 scripts/verify-website.py
- name: Record the published commit
run: |
-3
View File
@@ -50,9 +50,6 @@ 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/
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "6.2.0"
".": "6.1.0"
}
-243
View File
@@ -1,248 +1,5 @@
# 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)
+1 -1
View File
@@ -659,7 +659,7 @@ For a client that requires manual configuration, use:
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@6.2.0", "mcp"]
"args": ["-y", "rea-agents@6.1.0", "mcp"]
}
}
}
+2 -5
View File
@@ -294,11 +294,8 @@ 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. 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.
document base and query/fragment rules. 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
+1 -14
View File
@@ -77,15 +77,6 @@ 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.
@@ -188,9 +179,6 @@ increasing a client deadline alone does not fix those failures.
## Tool results
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
@@ -204,8 +192,7 @@ 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. Tool errors carry only their text
representation, so only that text counts against the budget. Response budget settings are captured at startup;
room for the JSON-RPC envelope. 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
-157
View File
@@ -1,157 +0,0 @@
# 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.
+3 -35
View File
@@ -112,17 +112,14 @@ 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`
ports from 0 through 65535. 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
Output, file count, file size, process sampling, filesystem depth, total
runtime, idle time, and post-exit settlement are bounded by the scenario's
limits. The result marks truncated observations and residual unknowns rather
than treating missing data as proof of equivalence. Cancellation and timeout
@@ -130,35 +127,6 @@ 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;
-3
View File
@@ -35,9 +35,6 @@ 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,
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "rea-agents",
"version": "6.2.0",
"version": "6.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rea-agents",
"version": "6.2.0",
"version": "6.1.0",
"license": "MIT",
"dependencies": {
"@babel/parser": "8.0.4",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "rea-agents",
"mcpName": "io.github.morluto/rea",
"version": "6.2.0",
"version": "6.1.0",
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
"license": "MIT",
"repository": {
@@ -232,7 +232,7 @@
"oxfmt",
"oxlint --fix"
],
"*.{json,md,yml,yaml}": "oxfmt --no-error-on-unmatched-pattern"
"*.{json,md,yml,yaml}": "oxfmt"
},
"dependencies": {
"@babel/parser": "8.0.4",
@@ -7,10 +7,6 @@ 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":');
-72
View File
@@ -1,14 +1,9 @@
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);
@@ -147,73 +142,6 @@ 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(
+2 -3
View File
@@ -1,4 +1,3 @@
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";
@@ -159,11 +158,11 @@ const verifyDefaultScreenshotDelivery = async ({
);
assert.equal(constrained.isError, true);
assert.equal(
requireMcpToolError(constrained)?.details?.resource,
constrained.structuredContent?.error?.details?.resource,
"transport",
);
const reference =
requireMcpToolError(constrained)?.details?.reported_limits
constrained.structuredContent?.error?.details?.reported_limits
?.evidence_reference;
assert.equal(reference?.kind, "retained-evidence");
await client.ping();
+1 -2
View File
@@ -35,7 +35,6 @@ 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",
@@ -77,7 +76,7 @@ const SOURCE_PATHS = {
nativeProvider: "dist/native/NativeMacOSProviderMetadata.js",
artifactProviders: "dist/application/InvestigationProviders.js",
browserIdentities: "dist/browser/providerIdentities.js",
v8InspectorProvider: "dist/inspector/providerIdentity.js",
v8InspectorProvider: "dist/inspector/V8InspectorProvider.js",
evidence: "dist/domain/evidence.js",
evidenceBundle: "dist/domain/evidenceBundle.js",
evidenceCompletion: "dist/domain/evidenceCompletionLedger.js",
@@ -1,8 +1,3 @@
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";
@@ -20,6 +15,10 @@ import {
} from "../../dist/domain/analysisSnapshot.js";
import { createEvidence } from "../../dist/domain/evidence.js";
import { createEvidenceBundle } from "../../dist/domain/evidenceBundle.js";
import {
requireMcpLifecycleResult,
requireMcpEvidenceResult,
} from "./mcp-verifier-results.mjs";
/** Reject a valid older snapshot whose instruction-only collector omitted a real edge. */
export async function verifyLegacyGhidraReferenceSnapshot(
@@ -125,7 +124,7 @@ export async function verifyLegacyGhidraReferenceSnapshot(
},
});
assert.equal(reply.isError, true);
const mcpRejection = requireMcpToolError(reply);
const mcpRejection = reply.structuredContent.error;
assert.equal(mcpRejection.code, "evidence_integrity_mismatch");
assert.match(mcpRejection.details.reason, /profile_mismatch/u);
assert.match(
-7
View File
@@ -1,4 +1,3 @@
import { analysisErrorProjectionSchema } from "../../dist/contracts/errorSchemas.js";
import { toolContract } from "../../dist/contracts/toolContracts.js";
/** Read the textual JSON projection from one MCP tool call. */
@@ -10,12 +9,6 @@ export const mcpTextValue = (result) => {
const jsonValue = (result) => JSON.parse(mcpTextValue(result));
/** 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)}`);
-9
View File
@@ -25,15 +25,6 @@ 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) => {
+1 -1
View File
@@ -24,7 +24,7 @@ export const createProductCatalog = async (root) => {
assertSameNames(
"Primary CLI inventory",
cli.primary,
sources.cliCommandNames.CLI_COMMAND_NAMES,
sources.catalogIdentity.CLI_COMMAND_NAMES,
);
const tools = toolFamilyCatalog(sources);
const providers = providerCatalog(sources);
@@ -1,4 +1,3 @@
import { requireMcpToolError } from "./mcp-verifier-results.mjs";
import assert from "node:assert/strict";
/** Exercise segmented-address truncation through real CLI/MCP reads and edits. */
@@ -41,14 +40,13 @@ export async function verifyGhidraAddressBoundaries(client, entry, cli) {
{ timeout: 240000 },
);
assert.equal(reply.isError, true, `${name} accepted ${address}`);
const error = requireMcpToolError(reply);
const error = reply.structuredContent?.error;
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);
// A rejected request produces no Evidence; errors carry text only.
assert.equal(reply.structuredContent, undefined);
assert.equal(reply.structuredContent.evidence, undefined);
}
}
assert.deepEqual(
+17 -16
View File
@@ -1,8 +1,3 @@
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";
@@ -12,9 +7,11 @@ 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,
requireMcpOperationResult,
} 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";
@@ -64,19 +61,23 @@ export async function verifyGhidraBoundaries(
true,
`${name} accepted ${JSON.stringify(args)}`,
);
// 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);
// 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)),
);
}
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 error;
return reply.structuredContent?.error;
};
const cli = async (command, value, flags = []) => {
const { stdout } = await promisify(execFile)(
@@ -480,7 +481,7 @@ export async function verifyGhidraBoundaries(
options,
);
assert.equal(regexFailure.isError, true);
const regexError = requireMcpToolError(regexFailure);
const regexError = regexFailure.structuredContent.error;
assert.equal(regexError.code, "resource_constraint");
assert.equal(regexError.details.operation, "search_strings");
assert.equal(regexError.details.resource, "memory");
@@ -502,7 +503,7 @@ export async function verifyGhidraBoundaries(
options,
);
assert.equal(compileFailure.isError, true);
const compileError = requireMcpToolError(compileFailure);
const compileError = compileFailure.structuredContent.error;
assert.equal(compileError.code, "resource_constraint");
assert.equal(compileError.details.operation, "search_procedures");
assert.match(compileError.details.reason, /stack while compiling pattern/u);
+4 -9
View File
@@ -64,13 +64,11 @@ export async function verifyGhidraLargeResults({
assert.equal(expected[0], 0x5a);
assert.equal(expected.at(-1), 0xa5);
const before = await call("binary_session");
// 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.
// This request fits the SDK receive limit, but repeating the producer's
// diagnostic in text and structured content cannot fit the response.
const missingSelector =
"REA_MISSING_" +
'"'.repeat(Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 4) + 64 * 1024);
"x".repeat(Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 2) + 64 * 1024);
const lookupError = await reject("procedure_address", {
procedure: missingSelector,
});
@@ -89,12 +87,9 @@ 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 / 4));
"x".repeat(Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 10));
const annotationError = await reject("annotate_native_function", {
procedure: entry.address,
name: "rea_frame_probe",
@@ -1,13 +1,10 @@
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 { requireMcpOperationResult } from "./mcp-verifier-results.mjs";
/** Keep mutable Ghidra observations out of immutable snapshots across target opens. */
export async function verifyGhidraSnapshotLifecycle(
@@ -34,7 +31,7 @@ export async function verifyGhidraSnapshotLifecycle(
true,
`${name} accepted a mutated snapshot binding`,
);
const error = requireMcpToolError(reply);
const error = reply.structuredContent?.error;
assert.equal(
error?.code,
"evidence_integrity_mismatch",
+1 -1
View File
@@ -1,5 +1,5 @@
import assert from "node:assert/strict";
import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProviderCapabilities.js";
import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProvider.js";
import {
requireMcpEvidenceResult,
requireEvidenceProvider,
+1 -1
View File
@@ -1,4 +1,4 @@
import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProviderCapabilities.js";
import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProvider.js";
/** Verify the selected Hopper candidate and concrete provider binding. */
export const requireHopperSelection = (status, expected) => {
+2 -75
View File
@@ -1,16 +1,10 @@
"""Prepare downloads, the sitemap and the sharing image for the static website."""
"""Package the public Notes example 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",
@@ -21,70 +15,6 @@ 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"
@@ -107,7 +37,4 @@ def prepare_notes_example(site):
if __name__ == "__main__":
root = Path(__file__).resolve().parent.parent
site = root / "website/public"
prepare_sharing_image(site)
prepare_sitemap(site)
prepare_notes_example(site)
prepare_notes_example(root / "website/public")
-130
View File
@@ -1,130 +0,0 @@
"""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()
+3 -4
View File
@@ -1,4 +1,3 @@
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";
@@ -233,12 +232,12 @@ const checkHardenedRuntime = async (directory, binary) => {
{ timeout: CLI_TIMEOUT_MS },
);
assert.equal(deniedMcp.isError, true, JSON.stringify(deniedMcp));
assert.equal(requireMcpToolError(deniedMcp).code, denied.code);
assert.equal(deniedMcp.structuredContent.error.code, denied.code);
assert.match(
requireMcpToolError(deniedMcp).details.reason,
deniedMcp.structuredContent.error.details.reason,
/^debugger-attach-denied: /u,
);
assert.ok(requireMcpToolError(deniedMcp).details.partial_observation);
assert.ok(deniedMcp.structuredContent.error.details.partial_observation);
});
const allowed = join(directory, "debuggable");
const entitlements = join(directory, "get-task-allow.plist");
+1 -2
View File
@@ -1,4 +1,3 @@
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";
@@ -51,7 +50,7 @@ const verifyMcpUnknownProvider = async (client, mcpOptions) => {
);
if (
unknownProvider.isError !== true ||
requireMcpToolError(unknownProvider)?.details?.selection_reason !==
unknownProvider.structuredContent?.error?.details?.selection_reason !==
"unknown_provider"
)
throw new Error("packaged MCP accepted an unknown analysis provider");
+1 -7
View File
@@ -16,7 +16,7 @@ import {
listBrowserTargetsInputSchema,
} from "../dist/domain/browserObservation.js";
import { observeWebSessionInputSchema } from "../dist/domain/browserSession.js";
import { compareWebCapturesInputSchema } from "../dist/domain/webCaptureDiffSchemas.js";
import { compareWebCapturesInputSchema } from "../dist/domain/webCaptureDiff.js";
import {
captureWebScreenshotInputSchema,
compareWebScreenshotsInputSchema,
@@ -33,7 +33,6 @@ import {
browserScenario,
runScenarioCli,
scenarioProfiles,
verifyScenarioFailureEvidence,
} from "./lib/browser-scenario-verifier.mjs";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
import { verifyLargeScreenshotE2e } from "./lib/browser-screenshot-e2e.mjs";
@@ -295,10 +294,6 @@ try {
if ([...profilesAfter].some((entry) => !profilesBefore.has(entry)))
throw new Error("Scenario launch retained a temporary browser profile");
assertScenarioCapture(launchedScenario.value);
const scenarioFailure = await verifyScenarioFailureEvidence(
executable,
site.origin,
);
const scenarioResults = JSON.stringify([
attachedScenario,
launchedScenario.value,
@@ -365,7 +360,6 @@ try {
browserScenarioCli: true,
browserScenarioAttachCleanup: "disconnected-external",
browserScenarioLaunchCleanup: "terminated-owned-process",
scenarioFailure,
verified: true,
};
} finally {
+1 -1
View File
@@ -3,7 +3,7 @@ import { hasTypedSwitchEvidence } from "./verify-real-ghidra-switch-assertions.m
import { access } from "node:fs/promises";
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraInstallationPolicy.js";
import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraInstallation.js";
import { GHIDRA_SESSION_CAPABILITIES } from "../dist/ghidra/GhidraSessionValues.js";
export function assertLocalProcedureInfo(info, expectedAddress) {
+5 -3
View File
@@ -16,10 +16,12 @@ import {
parseGhidraInventoryInput,
parseGhidraInventoryResult,
} from "../dist/ghidra/GhidraInventoryValues.js";
import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js";
import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraInstallationPolicy.js";
import {
inspectGhidraInstallation,
SUPPORTED_GHIDRA_VERSION,
} from "../dist/ghidra/GhidraInstallation.js";
import { GhidraHeadlessLauncher } from "../dist/ghidra/GhidraLauncher.js";
import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProviderCapabilities.js";
import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProvider.js";
import { windowsP0Capabilities } from "../dist/ghidra/GhidraProviderCapabilities.js";
import { GHIDRA_SESSION_CAPABILITIES } from "../dist/ghidra/GhidraSessionValues.js";
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
+5 -3
View File
@@ -10,10 +10,12 @@ import { promisify } from "node:util";
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
import { resolveGhidraAnalysisProfile } from "../dist/ghidra/GhidraAnalysisProfile.js";
import { GhidraClient } from "../dist/ghidra/GhidraClient.js";
import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js";
import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraInstallationPolicy.js";
import {
inspectGhidraInstallation,
SUPPORTED_GHIDRA_VERSION,
} from "../dist/ghidra/GhidraInstallation.js";
import { GhidraHeadlessLauncher } from "../dist/ghidra/GhidraLauncher.js";
import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProviderCapabilities.js";
import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProvider.js";
import {
assertCleanup,
+1 -1
View File
@@ -9,7 +9,7 @@ import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
import { resolveHopperAnalysisProfile } from "../dist/hopper/HopperAnalysisProfile.js";
import { HopperApplicationLauncher } from "../dist/hopper/BridgeLauncher.js";
import { HopperClient } from "../dist/hopper/HopperClient.js";
import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProviderCapabilities.js";
import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProvider.js";
import { loadRealHopperFixtureTargets } from "./lib/real-hopper-fixture.mjs";
// These options belong to the native client, below the CLI/MCP tool surface.
+1 -1
View File
@@ -24,7 +24,7 @@ import {
} from "./lib/mcp-verifier-results.mjs";
import { snapshotHopperRuntime } from "./lib/real-hopper-cleanup.mjs";
import { HOPPER_TARGET_LEASE_DIRECTORY } from "../dist/hopper/HopperTargetLease.js";
import { parseConfig } from "../dist/config/parseConfig.js";
import { parseConfig } from "../dist/config.js";
import {
verifyHopperSourceBinding,
relocateSingleFatSlice,
+1 -1
View File
@@ -11,7 +11,7 @@ import {
requireFunctionDossier,
requirePseudocode,
} from "../dist/application/RealHopperAssertions.js";
import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProviderCapabilities.js";
import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProvider.js";
import { HOPPER_TARGET_LEASE_DIRECTORY } from "../dist/hopper/HopperTargetLease.js";
import { REA_WORKFLOW_PROVIDER } from "../dist/application/InvestigationProviders.js";
import { loadRealHopperFixtureTargets } from "./lib/real-hopper-fixture.mjs";
+1 -2
View File
@@ -1,5 +1,4 @@
#!/usr/bin/env node
import { requireMcpToolError } from "./lib/mcp-verifier-results.mjs";
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
@@ -173,7 +172,7 @@ const mcp = async (cliDossier) => {
arguments: { address: "not-an-address" },
});
assert.equal(invalid.isError, true);
assert.equal(requireMcpToolError(invalid).code, "invalid_request");
assert.equal(invalid.structuredContent.error.code, "invalid_request");
await call("close_binary", {});
opened = false;
report.checks.push(
+10 -219
View File
@@ -1,19 +1,14 @@
"""Check website references, search metadata and the sole production publisher."""
"""Check local website references and the sole GitHub Pages publisher."""
from html.parser import HTMLParser
from pathlib import Path
import re
import struct
import sys
from urllib.parse import quote, unquote, urlsplit
from urllib.parse import unquote, urlsplit
import xml.etree.ElementTree as ET
from zipfile import BadZipFile, ZipFile
SITE_ORIGIN = "https://rea.tools"
NOTES_FIXTURES = {"examples/notes-electron/index.html", "examples/notes-web/index.html"}
class HtmlReferences(HTMLParser):
"""Collect document IDs and references without fetching external URLs."""
@@ -22,42 +17,9 @@ class HtmlReferences(HTMLParser):
self.ids = set()
self.references = []
self.duplicate_ids = []
self.in_head = False
self.in_title = False
self.titles = []
self.meta = {}
self.properties = {}
self.canonicals = []
self.lang = None
self.h1_count = 0
def handle_starttag(self, tag, attrs):
attributes = dict(attrs)
if tag == "html":
self.lang = attributes.get("lang")
if tag == "h1":
self.h1_count += 1
if tag == "head":
self.in_head = True
if self.in_head:
if tag == "title":
self.titles.append("")
self.in_title = True
if (
tag == "link"
and "canonical" in (attributes.get("rel") or "").lower().split()
):
self.canonicals.append(attributes.get("href") or "")
if tag == "meta":
for attribute, collection in (
("name", self.meta),
("property", self.properties),
):
key = attributes.get(attribute)
if key:
collection.setdefault(key.lower(), []).append(
attributes.get("content") or ""
)
identifier = attributes.get("id")
if identifier is not None:
if identifier in self.ids:
@@ -68,154 +30,6 @@ class HtmlReferences(HTMLParser):
if reference is not None:
self.references.append(reference)
def handle_endtag(self, tag):
if tag == "head":
self.in_head = False
if tag == "title":
self.in_title = False
def handle_data(self, data):
if self.in_title:
self.titles[-1] += data
def noindex(self):
"""Recognize directives that exclude this page from Google's index."""
directives = ",".join(
self.meta.get("robots", []) + self.meta.get("googlebot", [])
).lower()
return bool(set(re.split(r"[\s,]+", directives)) & {"noindex", "none"})
def check_search_metadata(site, pages):
"""Verify route-specific canonicals, sharing assets and the generated sitemap."""
errors = []
indexable = set()
titles = set()
for path, page in pages.items():
label = path.relative_to(site).as_posix()
title = page.titles[0].strip() if len(page.titles) == 1 else ""
if not title:
errors.append(f"{label}: needs exactly one nonempty head title")
if page.lang != "en" or page.h1_count != 1:
errors.append(f"{label}: needs lang=en and exactly one h1")
if label in NOTES_FIXTURES and not page.noindex():
errors.append(
f"{label}: example application must have a head noindex directive"
)
if page.noindex():
continue
route = (
path.relative_to(site).parent.as_posix()
if path.name == "index.html"
else label
)
route = "" if route == "." else route
url = f"{SITE_ORIGIN}/{quote(route, safe='/')}" + (
"/" if route and path.name == "index.html" else ""
)
indexable.add(url)
if page.canonicals != [url]:
errors.append(f"{label}: needs exactly one head canonical for {url}")
if title in titles:
errors.append(f"{label}: duplicate search title {title!r}")
titles.add(title)
descriptions = page.meta.get("description", [])
description = descriptions[0].strip() if len(descriptions) == 1 else ""
if not description:
errors.append(f"{label}: needs exactly one nonempty head description")
expected_og = {
"og:type": "website",
"og:site_name": "REA",
"og:title": title,
"og:description": description,
"og:url": url,
"og:image": f"{SITE_ORIGIN}/assets/social-card.png",
"og:image:type": "image/png",
"og:image:width": "1200",
"og:image:height": "630",
}
expected_twitter = {
"twitter:card": "summary_large_image",
"twitter:title": title,
"twitter:description": description,
"twitter:image": expected_og["og:image"],
}
for collection, expected in (
(page.properties, expected_og),
(page.meta, expected_twitter),
):
for key, value in expected.items():
if collection.get(key) != [value]:
errors.append(
f"{label}: {key} must appear once and match {value!r}"
)
for collection, key in (
(page.properties, "og:image:alt"),
(page.meta, "twitter:image:alt"),
):
values = collection.get(key, [])
if len(values) != 1 or not values[0].strip():
errors.append(f"{label}: needs exactly one nonempty {key}")
sitemap_path = site / "sitemap.xml"
try:
sitemap = ET.parse(sitemap_path).getroot()
namespace = "{http://www.sitemaps.org/schemas/sitemap/0.9}"
urls = [entry.findtext(f"{namespace}loc") for entry in sitemap]
if sitemap.tag != f"{namespace}urlset" or any(
entry.tag != f"{namespace}url" for entry in sitemap
):
errors.append("sitemap.xml: needs a sitemap-protocol urlset")
if len(urls) != len(set(urls)) or set(urls) != indexable:
errors.append(
"sitemap.xml: must contain each indexable page once, using its rea.tools URL"
)
if any(
len(entry) != 1 or entry[0].tag != f"{namespace}loc" for entry in sitemap
):
errors.append(
"sitemap.xml: keep entries to loc; do not invent lastmod or add priority/changefreq"
)
except (OSError, ET.ParseError) as error:
errors.append(
f"sitemap.xml: cannot read sitemap ({error}); run scripts/prepare-website.py"
)
robots_path = site / "robots.txt"
if not robots_path.is_file():
errors.append("robots.txt: authored crawl rules are missing")
else:
directives = []
for line in robots_path.read_text(encoding="utf-8").splitlines():
value = line.split("#", 1)[0].strip()
if value:
key, separator, content = value.partition(":")
directives.append(
(key.lower(), content.strip()) if separator else (value, "")
)
expected = [
("user-agent", "*"),
("allow", "/"),
("sitemap", f"{SITE_ORIGIN}/sitemap.xml"),
]
if directives != expected:
errors.append(
"robots.txt: expected crawl access and one absolute rea.tools sitemap directive"
)
image = site / "assets/social-card.png"
if not image.is_file():
errors.append("Social preview PNG is missing; run scripts/prepare-website.py")
else:
header = image.read_bytes()[:24]
if (
len(header) != 24
or header[:16] != b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR"
or struct.unpack(">II", header[16:24]) != (1200, 630)
):
errors.append("Social preview must be a 1200 × 630 PNG")
return len(indexable), errors
def check_site(site):
"""Check assets, HTML fragment destinations and SVG syntax."""
@@ -249,11 +63,7 @@ def check_site(site):
target /= "index.html"
if not target.is_file():
errors.append(f"{label}: missing local target: {reference}")
elif (
target in pages
and url.fragment
and unquote(url.fragment) not in pages[target].ids
):
elif target in pages and url.fragment and unquote(url.fragment) not in pages[target].ids:
errors.append(f"{label}: missing HTML fragment: {reference}")
for path in sorted(site.rglob("*.svg")):
@@ -261,23 +71,18 @@ def check_site(site):
ET.parse(path)
except ET.ParseError as error:
errors.append(f"{path.relative_to(site)}: invalid SVG XML: {error}")
indexable, metadata_errors = check_search_metadata(site, pages)
errors.extend(metadata_errors)
return len(pages), references, indexable, errors
return len(pages), references, errors
def check_publisher(root):
"""Detect a second Pages action that could replace the website."""
expected = root / ".github/workflows/website-pages.yml"
workflows = root / ".github/workflows"
deploy_action = re.compile(
r"^\s*-?\s*uses:\s*['\"]?actions/deploy-pages@", re.MULTILINE
)
deploy_action = re.compile(r"^\s*-?\s*uses:\s*['\"]?actions/deploy-pages@", re.MULTILINE)
publishers = [
path
for path in sorted(workflows.iterdir())
if path.suffix in (".yml", ".yaml")
and deploy_action.search(path.read_text(encoding="utf-8"))
if path.suffix in (".yml", ".yaml") and deploy_action.search(path.read_text(encoding="utf-8"))
]
if publishers != [expected]:
actual = ", ".join(str(path.relative_to(root)) for path in publishers) or "none"
@@ -290,14 +95,7 @@ def check_example_archive(site):
source = site / "examples/notes-electron"
expected = {
f"notes-example/{name}": source / name
for name in (
"package.json",
"main.js",
"preload.js",
"renderer.js",
"csv.js",
"index.html",
)
for name in ("package.json", "main.js", "preload.js", "renderer.js", "csv.js", "index.html")
}
path = site / "examples/notes-example.zip"
if not path.is_file():
@@ -305,9 +103,7 @@ def check_example_archive(site):
try:
with ZipFile(path) as archive:
if sorted(archive.namelist()) != sorted(expected):
return [
"Notes example ZIP must contain exactly the six files in notes-example/."
]
return ["Notes example ZIP must contain exactly the six files in notes-example/."]
return [
f"Notes example ZIP has stale source: {name}"
for name, original in expected.items()
@@ -320,18 +116,13 @@ def check_example_archive(site):
def main():
"""Run the same checks locally, on website PRs and before publishing."""
root = Path(__file__).resolve().parent.parent
pages, references, indexable, errors = check_site(root / "website/public")
pages, references, errors = check_site(root / "website/public")
errors.extend(check_example_archive(root / "website/public"))
errors.extend(check_publisher(root))
if errors:
print("Website checks failed:\n" + "\n".join(errors), file=sys.stderr)
return 1
print(
f"Verified {pages} HTML pages, {references} local references, SVG XML and the example ZIP."
)
print(
f"Verified search/sharing metadata, robots.txt and the sitemap for {indexable} indexable pages."
)
print(f"Verified {pages} HTML pages, {references} local references, SVG XML and the example ZIP.")
print("website-pages.yml is the sole GitHub Pages publisher.")
return 0
+8 -8
View File
@@ -1,4 +1,3 @@
import { requireMcpToolError } from "../../lib/mcp-verifier-results.mjs";
import assert from "node:assert/strict";
import {
access,
@@ -209,12 +208,13 @@ export async function verifyAndroidFailureRecovery({
{ timeout: 150_000 },
);
assert.equal(response.isError, true);
assert.equal(requireMcpToolError(response).code, "invalid_request");
assert.deepEqual(requireMcpToolError(response).details.issues[0].path, [
"class_name",
]);
assert.equal(response.structuredContent.error.code, "invalid_request");
assert.deepEqual(
response.structuredContent.error.details.issues[0].path,
["class_name"],
);
assert.match(
requireMcpToolError(response).details.issues[0].message,
response.structuredContent.error.details.issues[0].message,
/search_android_classes/u,
);
assert.doesNotMatch(JSON.stringify(response), /rea doctor/u);
@@ -231,8 +231,8 @@ export async function verifyAndroidFailureRecovery({
},
{ timeout: 150_000 },
);
assert.equal(requireMcpToolError(method).code, "invalid_request");
assert.deepEqual(requireMcpToolError(method).details.issues[0].path, [
assert.equal(method.structuredContent.error.code, "invalid_request");
assert.deepEqual(method.structuredContent.error.details.issues[0].path, [
"method_name",
]);
await client.ping();
@@ -1,86 +0,0 @@
import assert from "node:assert/strict";
import { chmod, readFile, stat } from "node:fs/promises";
import { relative, sep } from "node:path";
import { parseArtifactInventoryEvidence } from "../../../dist/domain/artifactInventoryEvidence.js";
import {
artifactCliEvidence,
withArtifactMcp,
} from "../../lib/artifact-e2e.mjs";
/** Verify native file roles and permissions on the real compiled bundle fixture. */
export async function verifyMachOOccurrences(app, executable) {
const originalMode = (await stat(executable)).mode & 0o7777;
const originalBytes = await readFile(executable);
const logicalPath = relative(app, executable).split(sep).join("/");
const observations = [];
try {
for (const mode of [0o755, 0o644]) {
await chmod(executable, mode);
const directEvidence = await artifactCliEvidence(
"inspect-artifact",
executable,
);
const direct = parseArtifactInventoryEvidence(directEvidence).inventory;
const containedEvidence = await artifactCliEvidence(
"inspect-artifact",
app,
);
const contained =
parseArtifactInventoryEvidence(containedEvidence).inventory;
assert.equal(direct.complete, true);
assert.equal(contained.complete, true);
const root = direct.occurrences.find(
({ logical_path }) => logical_path === ".",
);
const nested = contained.occurrences.find(
({ logical_path }) => logical_path === logicalPath,
);
assert.ok(root);
assert.ok(nested);
assert.ok(["mach-o", "mach-o-universal"].includes(root.artifact_format));
for (const occurrence of [root, nested]) {
assert.equal(occurrence.artifact_kind, "executable");
assert.equal(occurrence.artifact_format, direct.manifest.root_format);
assert.equal(occurrence.executable, mode === 0o755);
}
assert.equal(root.artifact_id, nested.artifact_id);
assert.equal(root.artifact_id, direct.manifest.root_artifact_id);
const node = contained.nodes.find(
({ artifact_id }) => artifact_id === nested.artifact_id,
);
assert.ok(node);
assert.equal(node.sha256, direct.manifest.root_sha256);
assert.equal(node.format, root.artifact_format);
await withArtifactMcp(app, async (client) => {
const reply = await client.callTool({
name: "inspect_artifact",
arguments: {},
});
assert.notEqual(reply.isError, true, JSON.stringify(reply));
assert.deepEqual(
parseArtifactInventoryEvidence(reply.structuredContent).inventory,
contained,
);
});
observations.push({
mode: mode.toString(8),
artifact_id: root.artifact_id,
artifact_kind: root.artifact_kind,
artifact_format: root.artifact_format,
executable: root.executable,
contained_cli_mcp_parity: true,
});
}
assert.equal(observations[0].artifact_id, observations[1].artifact_id);
assert.deepEqual(await readFile(executable), originalBytes);
return {
direct_cli: true,
contained_cli_mcp: true,
bytes_unchanged: true,
observations,
};
} finally {
await chmod(executable, originalMode);
}
}
+1 -4
View File
@@ -16,7 +16,6 @@ import {
} from "../../fixtures/apple/macos-bundle.mjs";
import { verifyDyldEnvironment } from "./dyld-environment.mjs";
import { verifyDylibResolution } from "./macos-dylib-e2e.mjs";
import { verifyMachOOccurrences } from "./macho-occurrences.mjs";
const exec = promisify(execFile);
@@ -68,8 +67,7 @@ const EXPECTED_BUNDLES = [
let report;
try {
const { app, executable } = await buildMacosBundleFixture(root);
const machoOccurrences = await verifyMachOOccurrences(app, executable);
const { app } = await buildMacosBundleFixture(root);
const zip = join(root, "MacFixture.zip");
await exec("/usr/bin/ditto", ["-c", "-k", "--keepParent", app, zip]);
const staging = join(root, "dmg-source");
@@ -166,7 +164,6 @@ try {
containers: containers.map(({ format }) => format),
bundles: EXPECTED_BUNDLES.length,
dmg_detached: true,
macho_occurrences: machoOccurrences,
dylib_resolution: dylibs,
dyld_environment: dyldEnvironment,
};
+2 -3
View File
@@ -1,4 +1,3 @@
import { requireMcpToolError } from "../../lib/mcp-verifier-results.mjs";
import assert from "node:assert/strict";
import { execFile, spawn } from "node:child_process";
import { randomUUID } from "node:crypto";
@@ -173,11 +172,11 @@ window.loadFile("renderer.html");require("node:fs").writeFileSync(${JSON.stringi
{ timeout: 150_000 },
);
const capture = active.isError
? requireMcpToolError(active).details.partial_observation.capture
? active.structuredContent.error.details.partial_observation.capture
: active.structuredContent.normalized_result;
if (active.isError) {
assert.equal(
requireMcpToolError(active).code,
active.structuredContent.error.code,
"cleanup_incomplete",
JSON.stringify(active),
);
+2 -2
View File
@@ -8,13 +8,13 @@
"url": "https://github.com/morluto/rea",
"source": "github"
},
"version": "6.2.0",
"version": "6.1.0",
"packages": [
{
"registryType": "npm",
"registryBaseUrl": "https://registry.npmjs.org",
"identifier": "rea-agents",
"version": "6.2.0",
"version": "6.1.0",
"runtimeHint": "npx",
"transport": {
"type": "stdio"
+1 -1
View File
@@ -16,7 +16,7 @@ import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import type { AndroidRequest } from "../domain/android/androidAnalysis.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import type { BinaryTarget } from "../domain/binaryTarget.js";
import { err, ok, type Result } from "../domain/result.js";
import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
import {
+1 -1
View File
@@ -9,7 +9,7 @@ import {
AnalysisInputError,
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import type { BinaryTarget } from "../domain/binaryTarget.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import type { OwnedProviderProcessSpawnOptions } from "../process/ProviderProcess.js";
+1 -1
View File
@@ -1,4 +1,4 @@
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import type { BinaryTarget } from "../domain/binaryTarget.js";
import type { OfficialToolName } from "../contracts/officialToolContracts.js";
import type { EnhancedToolName } from "../contracts/enhancedInputs.js";
import type { NativeToolName } from "../contracts/native/nativeToolContracts.js";
+2 -4
View File
@@ -7,13 +7,11 @@ import {
import { jsonValueSchema } from "../domain/jsonValue.js";
import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
import type {
BrowserTargetList,
InspectWebPageInput,
ListBrowserTargetsInput,
} from "../domain/browserObservation.js";
import type {
BrowserTargetList,
WebPageInspection,
} from "../domain/browserObservationSchemas.js";
} from "../domain/browserObservation.js";
import type {
AnalyzeWebBundleInput,
WebBundleAnalysis,
+2 -4
View File
@@ -1,12 +1,10 @@
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type {
BrowserTargetList,
InspectWebPageInput,
ListBrowserTargetsInput,
} from "../domain/browserObservation.js";
import type {
BrowserTargetList,
WebPageInspection,
} from "../domain/browserObservationSchemas.js";
} from "../domain/browserObservation.js";
import type { Result } from "../domain/result.js";
import type {
AnalyzeWebBundleInput,
+2 -3
View File
@@ -6,8 +6,7 @@ import type { DirectAnalysisDependencies } from "./DirectAnalysisDependencies.js
import type { BinarySession } from "./binary/BinarySession.js";
import type { SessionProviderRoute } from "./binary/SessionProviderRouter.js";
import type { ResolvedSessionOpen } from "./binary/BinarySessionOpen.js";
import { silentLogger } from "../logger.js";
import type { Logger } from "pino";
import { silentLogger, type Logger } from "../logger.js";
import { createEvidence } from "../domain/evidence.js";
import type { Evidence } from "../domain/evidence.js";
import type { NativeToolName } from "../contracts/native/nativeToolContracts.js";
@@ -25,7 +24,7 @@ import {
writeAnalysisSnapshot,
} from "./binary/AnalysisSnapshotFiles.js";
import { parseBinaryTarget } from "./BinaryTargetResolver.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import type { BinaryTarget } from "../domain/binaryTarget.js";
import {
snapshotEvidenceForQuery,
snapshotMatchesTarget,
@@ -1,6 +1,5 @@
import type { AppConfig } from "../config/types.js";
import type { parseConfig } from "../config/parseConfig.js";
import type { Logger } from "pino";
import type { AppConfig, parseConfig } from "../config.js";
import type { Logger } from "../logger.js";
import type { BinarySession } from "./binary/BinarySession.js";
/** Configuration and one-shot session factories supplied by the production boundary. */
+1 -2
View File
@@ -1,8 +1,7 @@
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { jsonObjectSchema, type JsonValue } from "../domain/jsonValue.js";
import { createServerIdentity } from "../serverIdentity.js";
import { silentLogger } from "../logger.js";
import type { Logger } from "pino";
import { silentLogger, type Logger } from "../logger.js";
import type { DirectAnalysisDependencies } from "./DirectAnalysisDependencies.js";
const runSessionStatus = async (
+2
View File
@@ -42,6 +42,8 @@ import { traceCallPath } from "./CallPathTracing.js";
import { resolveProcedureAddress } from "./ProcedureAddressResolution.js";
import { traceLiteralFeature } from "./EnhancedLiteralTracing.js";
import { projectNativeApiInspection } from "./native/NativeApiInspection.js";
export type { ValidatedEnhancedCall } from "./EnhancedToolTypes.js";
/**
* Composes direct provider operations into bounded reverse-engineering tools.
* Direct callers use `execute`; adapters that own validation use
+1 -1
View File
@@ -10,7 +10,7 @@ import {
} from "../domain/evidenceErrors.js";
import { err, ok, type Result } from "../domain/result.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { parseProcessCapture } from "../domain/process/processCaptureParsing.js";
import { parseProcessCapture } from "../domain/process/processCapture.js";
import {
bufferedJsonParts,
canonicalJsonParts,
+1 -1
View File
@@ -3,7 +3,7 @@ import type {
ExecutionOptions,
} from "./AnalysisProvider.js";
import { toolContract } from "../contracts/toolContracts.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import type { BinaryTarget } from "../domain/binaryTarget.js";
import { createEvidence } from "../domain/evidence.js";
import { parseFunctionDossier } from "../domain/hopperValues.js";
import { jsonObjectSchema, type JsonValue } from "../domain/jsonValue.js";
@@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest";
import { createEvidence } from "../domain/evidence.js";
import { createEvidenceBundle } from "../domain/evidenceBundle.js";
import { emptyUnverifiedProcessCapture } from "../domain/process/processCapture.fixture.js";
import { parseProcessCapture } from "../domain/process/processCaptureParsing.js";
import { parseProcessCapture } from "../domain/process/processCapture.js";
import {
digestProcessCommitment,
parseProcessScenario,
@@ -7,7 +7,7 @@ import {
parseEvidenceBundle,
type EvidenceBundle,
} from "../domain/evidenceBundle.js";
import { parseProcessCapture } from "../domain/process/processCaptureParsing.js";
import { parseProcessCapture } from "../domain/process/processCapture.js";
import type {
ReconstructionObligation,
ReviewedReconstructionObligation,
@@ -1,12 +1,13 @@
import { PROCESS_PROVIDER } from "../domain/process/processEvidenceProvider.js";
import { PROCESS_PROVIDER } from "./process/ProcessEvidence.js";
import {
buildReconstructionObligationLedgerEvidenceValidated,
resolveReconstructionObligationLedgerRequest,
} from "./ReconstructionObligationLedgerService.js";
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/process/processCaptureExample.js";
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/process/processCapture.fixture.js";
import { createEvidence, type Evidence } from "../domain/evidence.js";
import { createEvidenceBundle } from "../domain/evidenceBundle.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { processCaptureSchema } from "../domain/process/processCapture.js";
import {
reconstructionObligationLedgerSchema,
type ReconstructionObligationLedgerInput,
@@ -14,7 +15,6 @@ import {
type ReviewedReconstructionObligation,
} from "../domain/reconstructionObligationLedgerSchemas.js";
import { processCaptureSchema } from "../domain/process/processCapture.js";
/** Build controlled fixture Evidence used by reconstruction-ledger scenarios. */
export const proofEvidence = (id: string) =>
createEvidence(
+5
View File
@@ -17,6 +17,11 @@ import {
} from "./ReferenceSourceImportTypes.js";
import { prepareReferenceSourceImport } from "./ReferenceSourceImportPolicy.js";
export type {
ReferenceSourceImportError,
ReferenceSourceImportOptions,
} from "./ReferenceSourceImportTypes.js";
const failure = (
code: ReferenceSourceImportError["code"],
message: string,
@@ -10,7 +10,7 @@ import { parseReferenceSourceImports } from "../domain/referenceSourceImportPars
import type {
ReferenceSourceEntry,
ReferenceSourceRead,
} from "../reference/ReferenceSourceReaderTypes.js";
} from "../reference/ReferenceSourceReader.js";
import { PARSEABLE_REFERENCE_SOURCE_LANGUAGES } from "./ReferenceSourceImportTypes.js";
export interface ParsedReferenceSourceEntries {
+1
View File
@@ -40,6 +40,7 @@ describe("workflow residual question retention", () => {
required_authority: "shipped-artifact",
},
]);
expect(records.listUnknowns()).toHaveLength(1);
});
it("preserves registry failures rather than presenting a completed workflow", () => {
+1 -1
View File
@@ -9,7 +9,7 @@ import {
analysisProfileSchema,
committedProviderSchema,
} from "../domain/analysisProfile.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import type { BinaryTarget } from "../domain/binaryTarget.js";
import {
createEvidence,
type Evidence,
@@ -3,7 +3,7 @@ import type {
ExecutionOptions,
} from "../AnalysisProvider.js";
import type { AndroidRequest } from "../../domain/android/androidAnalysis.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type { Result } from "../../domain/result.js";
import type { ProviderAvailability } from "../AnalysisProvider.js";
@@ -2,7 +2,7 @@ import {
analysisProfileSchema,
type AnalysisProfileCommitment,
} from "../../domain/analysisProfile.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import {
AnalysisCancelledError,
AnalysisUnsupportedTargetError,
@@ -11,7 +11,7 @@ import {
type ProviderTargetSupport,
} from "../AnalysisProvider.js";
import { createAnalysisProfile } from "../../domain/analysisProfile.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { ProviderSelectionError } from "../../domain/providerSelectionError.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
@@ -4,7 +4,7 @@ import {
type AnalysisProviderSelector,
} from "../../contracts/providerSelection.js";
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
@@ -1,4 +1,4 @@
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import type { EvidenceBundle } from "../../domain/evidenceBundle.js";
+2 -1
View File
@@ -1,6 +1,6 @@
import { randomUUID } from "node:crypto";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import {
analysisProfilesEqual,
type AnalysisProfileCommitment,
@@ -48,6 +48,7 @@ import {
analysisErrorWithCleanupFailure,
closeAnalysisClient,
} from "./AnalysisClientCleanup.js";
export type { BinarySessionPort } from "./BinarySessionPort.js";
const OFFICIAL_OPERATIONS: ReadonlySet<string> = new Set(
OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name),
);
@@ -1,5 +1,5 @@
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
import { NoBinaryOpenError } from "../../domain/configurationErrors.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
+1 -1
View File
@@ -8,7 +8,7 @@ import {
} from "../../domain/analysisSnapshot.js";
import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js";
import { parseBinaryTarget } from "../BinaryTargetResolver.js";
import { type BinaryTarget } from "../../domain/binaryTargetTypes.js";
import { type BinaryTarget } from "../../domain/binaryTarget.js";
import {
EvidenceIntegrityError,
AnalysisSnapshotMismatchError,
+1 -1
View File
@@ -1,6 +1,6 @@
import type { InvestigationRecordPort } from "../investigation/InvestigationRecordPort.js";
import type { ExecutableFormatHint } from "../../domain/dosCom.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js";
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
@@ -1,6 +1,6 @@
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import type { Evidence } from "../../domain/evidence.js";
import type { EvidenceBundle } from "../../domain/evidenceBundle.js";
import type { JsonValue } from "../../domain/jsonValue.js";
@@ -1,4 +1,4 @@
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import type { ProviderOperationHealth } from "../../domain/providerOperationHealth.js";
import type {
+1 -1
View File
@@ -1,6 +1,6 @@
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
import { err, ok } from "../../domain/result.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
import type {
AnalysisClient,
@@ -1,5 +1,5 @@
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
@@ -1,6 +1,6 @@
import type { AnalysisProviderSelector } from "../../contracts/providerSelection.js";
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
import { ProviderSelectionError } from "../../domain/providerSelectionError.js";
import { type AnalysisError } from "../../domain/analysisErrorBase.js";
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import { createEvidence } from "../../domain/evidence.js";
import { createEvidenceBundle } from "../../domain/evidenceBundle.js";
import { recordUnknownInputSchema } from "../../domain/residualUnknown.js";
@@ -1,4 +1,4 @@
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import type { EvidenceBundle } from "../../domain/evidenceBundle.js";
import type { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
@@ -1,4 +1,4 @@
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import type {
RecordUnknownInput,
@@ -1,4 +1,4 @@
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
import { sha256Text } from "../../domain/javascript/javascriptStaticAnalysisHelpers.js";
import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
@@ -1,4 +1,4 @@
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
import type { ElectronSenderValidationFinding } from "../../domain/javascript/electronStaticAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
import {
@@ -1,4 +1,4 @@
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
import type {
ElectronBrowserWindowFinding,
ElectronContextBridgeFinding,
@@ -1,11 +1,9 @@
import {
createJavaScriptApplicationEdge,
createJavaScriptApplicationNode,
type ApplicationEdge,
type ApplicationNode,
} from "../../domain/javascript/javascriptApplicationGraph.js";
import type {
ApplicationEdge,
ApplicationNode,
} from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
/** Deduplicate graph entities while retaining distinct evidence observations. */
export class JavaScriptArtifactGraphAccumulator {
@@ -1,4 +1,4 @@
import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js";
import type { ArtifactInventorySnapshot } from "../../artifacts/inventory/ArtifactInventory.js";
import {
createJavaScriptApplicationGraph,
createImmutableJavaScriptApplicationGraphSteps,
@@ -36,10 +36,10 @@ import {
import {
addJavaScriptArtifactContainers,
addJavaScriptArtifactFiles,
addJavaScriptBundlerNodes,
addJavaScriptPackageNodes,
createJavaScriptArtifactRootNode,
} from "./JavaScriptArtifactGraphStructure.js";
import { addJavaScriptBundlerNodes } from "./JavaScriptArtifactGraphBundlers.js";
import { addElectronBoundaries } from "./ElectronBoundaryGraph.js";
import {
classifyElectronIpcPairings,
@@ -1,4 +1,4 @@
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
import type { JavaScriptBundlerRegistration } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
import {
@@ -1,10 +1,10 @@
import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js";
import type { ArtifactInventorySnapshot } from "../../artifacts/inventory/ArtifactInventory.js";
import {
completeApplicationCoverage,
partialApplicationCoverage,
type ApplicationGraphEvidence,
} from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import type {
@@ -1,4 +1,4 @@
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
import type { JavaScriptStaticAnalysis } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
import {
@@ -1,5 +1,5 @@
import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
import type { ArtifactInventorySnapshot } from "../../artifacts/inventory/ArtifactInventory.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
import { completeApplicationCoverage } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import type {
@@ -21,6 +21,7 @@ import {
staticInferenceEvidence,
} from "./JavaScriptArtifactGraphEvidence.js";
import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js";
export { addJavaScriptBundlerNodes } from "./JavaScriptArtifactGraphBundlers.js";
interface PackageRoleInput {
readonly packageNode: ApplicationNode;
@@ -5,9 +5,7 @@ import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptA
import {
admitsCanonicalPathSyntax,
hasScheme,
htmlUrlText,
looksExternal,
percentDecodeUrlPath,
stripQueryAndFragment,
} from "../../domain/artifactPathSyntax.js";
@@ -250,7 +248,7 @@ const hasContainerCandidate = (
const htmlCandidate = (
input: ResolveArtifactPathInput,
): string | ArtifactPathResolution => {
const declared = stripQueryAndFragment(htmlUrlText(input.declaredPath));
const declared = stripQueryAndFragment(input.declaredPath);
if (looksExternal(declared))
return unresolvedOutcome(input, "external", [
"External HTML references are not mapped to local artifact assets.",
@@ -259,25 +257,14 @@ const htmlCandidate = (
const base =
rawBase === undefined || rawBase === null
? rawBase
: stripQueryAndFragment(htmlUrlText(rawBase));
: stripQueryAndFragment(rawBase);
if (base !== undefined && base !== null && looksExternal(base))
return unresolvedOutcome(input, "external", [
"The document base href is external, so its script reference is not a local artifact path.",
]);
// Removing URL tabs and newlines can join an encoded dot or separator that
// the raw declaration split, so admit the parsed URL text again.
if (!admitsCanonicalPathSyntax(declared))
return unresolvedOutcome(input, "rejected", [
"Encoded dot or separator bytes are rejected before artifact path resolution.",
]);
const declaredPath = percentDecodeUrlPath(declared);
if (declaredPath === null)
return unresolvedOutcome(input, "rejected", [
"The HTML reference path percent-decodes to NUL or to bytes that are not UTF-8.",
]);
if (declaredPath.startsWith("/")) return declaredPath.slice(1);
if (declared.startsWith("/")) return declared.slice(1);
if (base === undefined || base === null || base === "")
return posix.join(posix.dirname(input.sourcePath), declaredPath);
return posix.join(posix.dirname(input.sourcePath), declared);
// A local base href is a second untrusted path input; apply the same
// admission rules a declared path gets so it cannot smuggle traversal or
// separator syntax past canonicalization.
@@ -285,15 +272,10 @@ const htmlCandidate = (
return unresolvedOutcome(input, "rejected", [
"The document base href uses NUL, backslash, or encoded dot and separator bytes that are not admitted for canonical artifact paths.",
]);
const decodedBase = percentDecodeUrlPath(base);
if (decodedBase === null)
return unresolvedOutcome(input, "rejected", [
"The document base href path percent-decodes to NUL or to bytes that are not UTF-8.",
]);
const basePath = decodedBase.startsWith("/")
? decodedBase.slice(1)
: posix.join(posix.dirname(input.sourcePath), decodedBase);
return posix.join(htmlBaseDirectory(decodedBase, basePath), declaredPath);
const basePath = base.startsWith("/")
? base.slice(1)
: posix.join(posix.dirname(input.sourcePath), base);
return posix.join(htmlBaseDirectory(base, basePath), declared);
};
/**
@@ -18,7 +18,7 @@ import {
javascriptArtifactReconstructionInputSchema,
type JavaScriptArtifactReconstructionInput,
} from "./JavaScriptArtifactReconstructionInput.js";
import { scanCanonicalArtifactInventory } from "../../artifacts/inventory/scanCanonical.js";
import { scanCanonicalArtifactInventory } from "../../artifacts/inventory/ArtifactInventory.js";
import { summarizeElectronBoundaries } from "./ElectronBoundaryAnalysis.js";
import { createJavaScriptSemanticGraphProjection } from "./JavaScriptSemanticGraphBuilder.js";
import type { ProgressReporter } from "../ProgressReporter.js";
@@ -109,73 +109,3 @@ describe("HTML base href path syntax", () => {
});
});
});
describe("HTML reference URL text", () => {
const urlPath = (declared: string, base: string) =>
decodeURIComponent(
new URL(declared.trim(), new URL(base.trim(), documentUrl)).pathname,
).replace(/^\/+/, "");
it.each([
["%61pp.js", "/assets/", "assets/app.js"],
[" app.js ", "/assets/", "assets/app.js"],
["\napp.js\t", "/assets/", "assets/app.js"],
["a\npp.js", "/assets/", "assets/app.js"],
["app.js", "%61/", "renderer/a/app.js"],
["app.js", " /%61ssets/ ", "assets/app.js"],
["/%61ssets/app.js", "", "assets/app.js"],
])(
"resolves %j against base %j like the URL parser",
(declared, base, expected) => {
if (base !== "") expect(urlPath(declared, base)).toBe(expected);
expect(resolve(declared, base)).toMatchObject({
declared_path: declared,
resolution_status: "resolved",
resolved_path: expected,
});
},
);
it("keeps a literal percent sign that starts no escape", () => {
expect(resolve("100%.js", "")).toMatchObject({
resolution_status: "not-found",
limitations: [
"The exact HTML resource renderer/100%.js was not found among the selected application files.",
],
});
});
it.each(["%00app.js", "%ff.js"])(
"rejects %j, which decodes to no canonical artifact path",
(declared) => {
expect(resolve(declared, "")).toMatchObject({
resolution_status: "rejected",
resolved_path: null,
});
},
);
it("rejects a base href whose URL whitespace splits an encoded separator", () => {
expect(resolve("app.js", "a%2\nf/")).toMatchObject({
resolution_status: "rejected",
resolved_path: null,
});
});
it("rejects a base href that decodes to NUL", () => {
expect(resolve("app.js", "/a%00/")).toMatchObject({
resolution_status: "rejected",
resolved_path: null,
});
});
it.each([" https://external.test/app.js", "\t//external.test/app.js"])(
"reports %j as external after removing URL whitespace",
(declared) => {
expect(resolve(declared, "")).toMatchObject({
resolution_status: "external",
});
},
);
it.each(["%2e%2e/app.js", "a%2fapp.js", "%2\nfapp.js", "%2\ne%2\te/app.js"])(
"still rejects encoded dot and separator bytes in %j",
(declared) => {
expect(resolve(declared, "")).toMatchObject({
resolution_status: "rejected",
});
},
);
});
@@ -1,7 +1,7 @@
import { posix } from "node:path";
import { compareCodePoints } from "../../domain/canonicalOrdering.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
import {
completeApplicationCoverage,
partialApplicationCoverage,
@@ -1,5 +1,5 @@
import { createJavaScriptSemanticGraphUnknown } from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraph.js";
import type {
JavaScriptSemanticEventOperation,
JavaScriptSemanticTimerOperation,
@@ -10,7 +10,10 @@ import {
addSemanticGraphUnknown,
} from "./JavaScriptSemanticGraphConstruction.js";
import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js";
import {
semanticCallSiteAt,
type SemanticFlowProjectionContext,
} from "./JavaScriptSemanticGraphFlowProjection.js";
/** Project literal EventEmitter registrations, removals, and dispatches. */
export const projectSemanticEvents = (
@@ -172,7 +175,7 @@ const projectTimerSchedule = (
timerNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
): void => {
addSemanticGraphRelation(context.state, {
source: context.callSiteAt(operation.location),
source: semanticCallSiteAt(context, operation.location),
target: timerNodes.get(operation.timerId),
relation: "schedules-timer",
resolution: "resolved",
@@ -196,7 +199,7 @@ const projectTimerCancellation = (
operation: JavaScriptSemanticTimerOperation,
timerNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
): void => {
const callSite = context.callSiteAt(operation.location);
const callSite = semanticCallSiteAt(context, operation.location);
const target =
operation.linkedTimerId === null
? undefined
@@ -838,85 +838,6 @@ it("retains array length presence without inventing an exact length", () => {
).toBe(false);
});
it.each([
["object spread", 'const root = { known: "value", ...dynamic };'],
["array spread", 'const root = ["value", ...dynamic];'],
])(
"keeps omitted initializer coverage on the owning binding: %s",
(_label, source) => {
const graph = graphFor(source);
const root = graph.nodes.find(
({ kind, label }) => kind === "binding" && label === "root",
);
if (root === undefined) throw new Error("Expected root binding");
const result = queryJavaScriptSemanticGraph(graph, {
seed: { kind: "semantic-node", node_id: root.node_id },
direction: "forward-influence",
});
expect(result.coverage.status).toBe("partial");
expect(result.unknowns).toContainEqual(
expect.objectContaining({
node_id: root.node_id,
family: "object-flow",
reason: "ambiguous-target",
detail: expect.stringContaining("Initializer container has unknown"),
}),
);
},
);
it.each([
["bare if", 'if (flag) alias = { mode: "other" }; alias.mode = "updated";'],
[
"logical expression",
'flag && (alias = { mode: "other" }); alias.mode = "updated";',
],
[
"logical assignment",
'flag ||= (alias = { mode: "other" }); alias.mode = "updated";',
],
["or assignment", 'alias ||= { mode: "other" }; alias.mode = "updated";'],
[
"nullish assignment",
'alias ??= { mode: "other" }; alias.mode = "updated";',
],
[
"conditional mutation",
'alias = { mode: "other" }; if (flag) alias.mode = "updated";',
],
[
"short-circuit mutation",
'alias = { mode: "other" }; flag && (alias.mode = "updated");',
],
])(
"preserves possible alias mutation uncertainty in queries: %s",
(_label, body) => {
const graph = graphFor(`
const shared = { mode: "initial" };
let alias = shared;
${body}
`);
const sharedMode = graph.nodes.find(
({ kind, identity, properties }) =>
kind === "property-slot" &&
properties.property_pointer === "/mode" &&
identity.role_key.includes("binding:shared"),
);
if (sharedMode === undefined) throw new Error("Expected shared mode slot");
const result = queryJavaScriptSemanticGraph(graph, {
seed: { kind: "semantic-node", node_id: sharedMode.node_id },
direction: "backward-provenance",
});
expect(sharedMode.properties.presence).toBe("unknown-coverage");
expect(result.nodes).toContainEqual(
expect.objectContaining({ node_id: sharedMode.node_id }),
);
expect(result.coverage.status).toBe("partial");
},
);
it.each(["{}", "unknownRoot", "{ a: null }"])(
"preserves the requested deep leaf name after a blocked prefix: %s",
(initializer) => {
@@ -2,8 +2,8 @@ import {
createJavaScriptSemanticGraph,
createImmutableJavaScriptSemanticGraphSteps,
type JavaScriptSemanticGraph,
type JavaScriptSemanticGraphNode,
} from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import {
JAVASCRIPT_SEMANTIC_RELATION_FAMILIES,
JAVASCRIPT_SEMANTIC_RELATION_FAMILY,
@@ -1,5 +1,5 @@
import { createJavaScriptSemanticGraphUnknown } from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraph.js";
import type {
JavaScriptSemanticChildProcessInteraction,
JavaScriptSemanticChildProcessSpawn,
@@ -10,7 +10,10 @@ import {
addSemanticGraphUnknown,
} from "./JavaScriptSemanticGraphConstruction.js";
import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js";
import {
semanticCallSiteAt,
type SemanticFlowProjectionContext,
} from "./JavaScriptSemanticGraphFlowProjection.js";
interface ChildUnknownInput {
readonly context: SemanticFlowProjectionContext;
@@ -72,7 +75,7 @@ const projectSpawn = (
spawn: JavaScriptSemanticChildProcessSpawn,
child: JavaScriptSemanticGraphNode,
): void => {
const callSite = context.callSiteAt(spawn.location);
const callSite = semanticCallSiteAt(context, spawn.location);
addSemanticGraphRelation(context.state, {
source: callSite,
target: child,
@@ -5,8 +5,8 @@ import {
createJavaScriptSemanticGraphNode,
createJavaScriptSemanticGraphRelation,
javaScriptSemanticNodeId,
type JavaScriptSemanticGraphNode,
} from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import type { ApplicationGraphEvidence } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
import type {
JavaScriptSemanticGraphRelation,
@@ -1,5 +1,5 @@
import { createJavaScriptSemanticGraphUnknown } from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraph.js";
import type {
JavaScriptSemanticBoundaryOperation,
JavaScriptSemanticConfigurationOperation,
@@ -11,7 +11,10 @@ import {
addSemanticGraphUnknown,
} from "./JavaScriptSemanticGraphConstruction.js";
import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js";
import {
semanticCallSiteAt,
type SemanticFlowProjectionContext,
} from "./JavaScriptSemanticGraphFlowProjection.js";
/** Project environment, argv, file, default, and precedence candidates. */
export const projectSemanticConfiguration = (
@@ -155,7 +158,7 @@ const projectRequestConstruction = (
operation: JavaScriptSemanticRequestOperation,
request: JavaScriptSemanticGraphNode,
): void => {
const callSite = context.callSiteAt(operation.location);
const callSite = semanticCallSiteAt(context, operation.location);
addSemanticGraphRelation(context.state, {
source: callSite,
target: request,
@@ -194,7 +197,7 @@ const projectResponseConsumer = (
): void => {
addSemanticGraphRelation(context.state, {
source: response,
target: context.callSiteAt(operation.location),
target: semanticCallSiteAt(context, operation.location),
relation: "consumed-by",
resolution: operation.resolution === "complete" ? "resolved" : "candidate",
properties: { method: operation.method },
@@ -1,5 +1,5 @@
import type { ApplicationGraphEvidence } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
@@ -101,7 +101,7 @@ export const unavailableSemanticRootEvidence = (
/** Explicit unknown evidence at one unresolved dynamic syntax location. */
export const unknownSemanticEvidence = (
file: JavaScriptArtifactFile,
location: JavaScriptSourceRange | null,
location: JavaScriptSourceRange,
): ApplicationGraphEvidence => ({
...observedSemanticEvidence(file, location),
authority: "unknown",

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