Compare commits

...
Author SHA1 Message Date
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>N0zoM1z0
a64851f259 chore(release/6.2.0): release rea-agents 6.2.0 (#1305)
* chore(release/6.2.0): release rea-agents 6.2.0

* docs(release): complete 6.2 breaking migration notes

* fix(dev): allow formatter-ignored staged files

* docs(release): include all unreleased mainline entries

* docs(release): note ignored-file hook repair

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: N0zoM1z0 <1705004398@qq.com>
2026-10-09 16:17:20 +00:00
YsopwnableandGitHub c267e99227 docs: explain 6.2 contract and saved-data migration (#1304) 2026-10-09 15:31:11 +00:00
YsopwnableandGitHub 4bb4c6fe6d test(apple): verify compiled Mach-O root occurrence facts (#1300)
test(artifacts): verify compiled Mach-O roles and permissions

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-09 15:24:53 +00:00
You WishandGitHub 411498d45f fix(electron): resolve effective preload after option overrides (#1286) 2026-10-09 08:05:00 -07:00
Kaoru0822andGitHub c3fc6c3aed Merge pull request #1297 from morluto/fix/ghidra-session-root-stability
fix(ghidra): use dot-free private session paths
2026-10-09 07:59:06 -07:00
Kaoru0822andGitHub 1f099a6fc0 Merge pull request #1299 from morluto/fix/schema-patterns-without-lookaround
fix(mcp): advertise input patterns without look-around
2026-10-09 07:58:51 -07:00
Kaoru0822andGitHub 701d90aba4 Merge pull request #1298 from morluto/test/artifacts-direct-macho-root-kind
test(artifacts): pin per-occurrence roles for direct Mach-O roots and shared bytes
2026-10-09 07:54:24 -07:00
Kaoru0822andClaude Opus 5.5 a8701eaf48 fix(mcp): advertise input patterns without look-around
trace_dylib_resolution roots and capture_process_scenario environment
names still advertised negative look-ahead patterns. RE2 and Rust schema
validators reject look-around, so a client using one refuses the whole
tools request, as #1282 reported for the NUL escape. Spell both
constraints as equivalent look-around-free patterns and assert that no
advertised pattern uses look-around or backreferences.

Closes #1282

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:49:22 +00:00
morluto ad59531a96 refactor: remove forwarding exports and redundant aliases 2026-10-09 22:46:09 +08:00
morluto 69c3b32a21 test(mcp): read bundle failures from public error content 2026-10-09 22:46:09 +08:00
morluto 5f749c2096 fix(mcp): only recommend exporting retained evidence 2026-10-09 22:46:09 +08:00
morluto 76b214e45f fix(windows): retain unsettled jobs for cleanup retry 2026-10-09 22:46:09 +08:00
morluto 614e0c2e74 fix(browser): retain scenario observations across failures 2026-10-09 22:46:08 +08:00
morluto 73e98b2516 fix(javascript): preserve alias and container uncertainty 2026-10-09 22:46:08 +08:00
YsopwnableandGitHub 99f257bbf0 Merge pull request #1296 from morluto/feat/website-seo-metadata
feat(website): add search metadata and automatic sitemap
2026-10-09 22:43:41 +08:00
Kaoru0822andClaude Opus 5.5 b9e29c52d4 test(artifacts): pin per-occurrence roles for direct Mach-O roots and shared bytes
Both reported failures were fixed by 63bf9140, which moved kind and
executable facts from content nodes to occurrences, but neither
reproduction had a regression test. Pin the direct-file Mach-O root kind
and permission against its bundle occurrence, and the Apple JavaScript
component and runtime family next to byte-identical text resources.

Refs #1234
Refs #1235

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:39:51 +00:00
N0zoM1z0 1c3766a8bc test(ghidra): preserve selected verification resource limits 2026-10-09 14:37:06 +00:00
Kaoru0822andGitHub 098d76900e Merge pull request #1291 from morluto/fix/process-port-normalization-sentence-period
fix(process): normalize ports that end a sentence
2026-10-09 07:34:54 -07:00
Kaoru0822andGitHub 2ddd9456cd Merge pull request #1295 from morluto/fix/javascript-html-url-whitespace-admission
fix(javascript): reject encoded separators joined by HTML URL whitespace
2026-10-09 07:34:39 -07:00
N0zoM1z0 43405f9ffe Merge remote-tracking branch 'origin/main' into feat/website-seo-metadata 2026-10-09 22:33:04 +08:00
Kaoru0822andGitHub 6020a0b368 Merge pull request #1294 from morluto/test/process-contract-rejections-concurrent
test(process): check malformed capture choices concurrently
2026-10-09 07:31:08 -07:00
Kaoru0822andGitHub fe34ec2ef3 Merge pull request #1288 from morluto/fix/mcp-error-text-budget
fix(mcp): measure tool errors against their text-only delivery
2026-10-09 07:30:48 -07:00
Kaoru0822andClaude Opus 5.5 ad813e473b fix(process): treat non-ASCII letters as port continuations
\w is ASCII-only even with the u flag, so "port: 8080.β" was treated as
a sentence-ending port. Use Unicode letter and number classes for the
port boundary checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:28:19 +00:00
N0zoM1z0 1e341973cf test(ghidra): size oversized annotations for canonical delivery 2026-10-09 14:21:38 +00:00
Kaoru0822andGitHub 69833fb0d0 Merge pull request #1293 from morluto/fix/process-capture-terminal-byte-accounting
fix(process): validate terminal byte totals against retained frames
2026-10-09 07:20:08 -07:00
Kaoru0822andClaude Opus 5.5 184ed970b9 fix(process): keep ports before a chunk-final period
PTY chunks are normalized independently. A period that ends a chunk may
begin a decimal continued in the next chunk ("port: 80." then "5"), so
leave such a port unnormalized instead of masking the decimal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:19:37 +00:00
N0zoM1z0 8c9c5550d0 Merge remote-tracking branch 'origin/main' into feat/website-seo-metadata 2026-10-09 22:19:04 +08:00
Kaoru0822andClaude Opus 5.5 fb57c8643f fix(javascript): reject encoded separators joined by HTML URL whitespace
#1287 removes URL tabs and newlines from an HTML script URL before
percent-decoding. A declaration such as %2<newline>fapp.js passed the
raw admission check and then decoded to a separator, resolving to a
file the encoded-separator rule must refuse. Apply canonical path
admission again to the parsed URL text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:18:39 +00:00
Kaoru0822andClaude Opus 5.5 48de11ce2e test(process): check malformed capture choices concurrently
The malformed process-choice acceptance test spawned 26 CLI processes one
after another. On CI it took 45-59 s against its 60 s deadline and timed
out on unrelated PRs. Run the independent cases concurrently, each with
its own scenario file; every assertion is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:17:27 +00:00
Kaoru0822andGitHub b80bd2db96 Merge pull request #1289 from morluto/fix/ghidra-address-verifier-error-envelope
fix(ghidra): stop dereferencing the removed error payload in address verification
2026-10-09 07:14:32 -07:00
Kaoru0822andGitHub d601b8016e Merge pull request #1287 from morluto/fix/javascript-html-script-url-decoding
fix(javascript): resolve HTML script URLs after URL whitespace and percent decoding
2026-10-09 07:13:13 -07:00
Kaoru0822andClaude Opus 5.5 39eb0587c9 fix(process): validate terminal byte totals against retained frames
parseProcessCapture accepted terminal retention accounting that its
retained frames contradict: any retained_bytes within budget, and
observed_bytes above retained_bytes with no omitted frame. A malformed
capture could therefore report truncated: false while its terminal
evidence was incomplete. Recompute retained raw and rendered bytes from
the frames, and require equal observed and retained bytes when no frame
was omitted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:12:05 +00:00
Kaoru0822andClaude Opus 5.5 db98a51dd1 fix(process): normalize ports that end a sentence
Contextual port normalization refused any port followed by a period, so
common log lines such as "Listening on port: 8080." or
"http://localhost:8080." kept their run-specific port and equivalent
captures compared as changed. Accept a sentence-ending period while
still refusing a period that continues a decimal, version, or host name,
and bump the committed port-normalization version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:06:41 +00:00
Kaoru0822andClaude Opus 5.5 59fbaa2d33 fix(ghidra): stop dereferencing the removed error payload in address verification
Since #1277 an MCP tool error carries no structuredContent, so the real
Ghidra address-boundary verifier threw a TypeError on its first expected
rejection and never checked the remaining representations or the
alias-preservation control. Assert that the error carries no structured
payload instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:04:35 +00:00
Kaoru0822andClaude Opus 5.5 7b51121f7c fix(mcp): measure tool errors against their text-only delivery
Since #1277 an error result reaches the client without structuredContent,
but toErrorToolResult and the transport recovery still budgeted it as
structured JSON plus escaped text. An error whose text fit between about
half and all of the response budget was replaced by resource_constraint,
and without Evidence retention the original diagnostic was lost.

Count only the escaped text for errors. The boundary, process-global and
real Ghidra large-result probes now use diagnostics that exceed the
text-only budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 14:03:12 +00:00
YsopwnableandGitHub 33bf585185 fix(process): localize capture truncation diagnostics (#1284)
Report per-source capture coverage and existing file-hash omissions while preserving comparable observations in unaffected dimensions. Keep legacy truncated captures conservative and retain known partial trace events.

Closes #1250
Partially addresses #1251; independent hashing remains out of scope.
2026-10-09 21:56:43 +08:00
N0zoM1z0 f61673fa99 Merge remote-tracking branch 'origin/main' into fix/ghidra-session-root-stability 2026-10-09 13:54:37 +00:00
YsopwnableandGitHub 7e96f25f8b fix(mcp): deliver typed errors without invalid structured output (#1277)
* fix(mcp): deliver tool errors within advertised schema contracts

Retain complete typed JSON error content while omitting non-conforming
structured errors at the public transport boundary. Recover oversized
diagnostics into Evidence before removing their private structured carrier.
Update schema-validated client readers and cover actual stdio delivery.

Closes #1261

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)

* test(mcp): read typed errors in canonical Evidence regressions

* test(mcp): validate public error diagnostics in acceptance workflows
2026-10-09 13:54:32 +00:00
Kaoru0822andClaude Opus 5.5 502528c58d fix(javascript): resolve HTML script URLs after URL whitespace and percent decoding
An HTML script src or base href with percent-encoded letters or
surrounding whitespace was looked up literally, so an inventoried script
was reported as not-found. Apply HTML attribute and URL parser
whitespace handling, then percent-decode the path as the URL standard
does. Encoded dot and separator bytes are still rejected, and paths that
decode to NUL or non-UTF-8 bytes are rejected explicitly.

Closes #1236

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:52:55 +00:00
N0zoM1z0 bb8ff2cfba docs(website): explain SEO maintenance and indexing follow-up
Document the Python preview environment, automatic sitemap updates, canonical URLs, generated sharing card and Search Console checks after publication. Apply the website's clear, concise writing standard to search and sharing metadata.
2026-10-09 21:47:08 +08:00
N0zoM1z0 b8851cb977 feat(website): add search metadata and automatic sitemap
Prefer rea.tools in page canonicals and search listings. Generate the sitemap from current HTML indexing directives and render a shared PNG preview from SVG before verification and manual deployment.

Refs #1283
2026-10-09 21:46:05 +08:00
N0zoM1z0 2060a6d7f3 Merge branch 'fix/mcp-error-schema-delivery' into fix/ghidra-session-root-stability 2026-10-09 13:42:28 +00:00
N0zoM1z0 09058ebc4a test(mcp): validate public error diagnostics in acceptance workflows 2026-10-09 13:42:08 +00:00
N0zoM1z0 1e83d270c2 test(ghidra): use host-native absolute session root fixtures 2026-10-09 13:20:49 +00:00
felipeatpandN0zoM1z0 a8b8288396 fix: keep the Ghidra session root free of dot-prefixed path elements
Ghidra validates every project path element with NamingUtilities.checkName and aborts the headless analyzer on any element starting with '.', so an inherited TMPDIR such as ~/.cache/<tool> failed every Ghidra operation with a generic analysis failure. Select the session root through a helper that prefers the inherited base, then a dot-free platform temp directory, then the nearest dot-free ancestor.
2026-10-09 13:10:30 +00:00
N0zoM1z0 5486860673 Merge remote-tracking branch 'origin/main' into fix/mcp-error-schema-delivery 2026-10-09 13:06:26 +00:00
N0zoM1z0 fd46cff017 test(mcp): read typed errors in canonical Evidence regressions 2026-10-09 13:03:28 +00:00
N0zoM1z0 7bf63ed7f8 chore: integrate canonical Evidence changes for MCP error delivery 2026-10-09 12:43:17 +00:00
N0zoM1z0 da606ff777 fix(mcp): deliver tool errors within advertised schema contracts
Retain complete typed JSON error content while omitting non-conforming
structured errors at the public transport boundary. Recover oversized
diagnostics into Evidence before removing their private structured carrier.
Update schema-validated client readers and cover actual stdio delivery.

Closes #1261

Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
2026-10-09 12:26:27 +00:00
549 changed files with 5625 additions and 1688 deletions
+17 -5
View File
@@ -5,8 +5,7 @@ on:
branches: [main, "release/*"]
paths:
- "website/**"
- "scripts/verify-website.py"
- "scripts/prepare-website.py"
- "scripts/*website*.py"
- ".github/workflows/**"
workflow_dispatch:
@@ -21,11 +20,24 @@ jobs:
check:
name: Website checks
runs-on: ubuntu-latest
timeout-minutes: 5
timeout-minutes: 10
steps:
- name: Check out the repository
uses: actions/checkout@v4
- name: Prepare the example download
- name: Set up Python for website assets
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
cache-dependency-path: website/requirements.txt
- name: Install sharing-image dependencies
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends -y libcairo2 fonts-dejavu-core
python3 -m pip install -r website/requirements.txt
- name: Prepare website assets
run: python3 scripts/prepare-website.py
- name: Check website links and publishing configuration
- name: Check website links, metadata and publishing configuration
run: python3 scripts/verify-website.py
- name: Check sitemap discovery and metadata regressions
run: python3 scripts/test-website.py
+14 -3
View File
@@ -23,7 +23,7 @@ jobs:
prepare:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 5
timeout-minutes: 10
permissions:
contents: read
pages: read
@@ -48,9 +48,20 @@ jobs:
fi
- name: Check out the repository
uses: actions/checkout@v4
- name: Prepare the example download
- name: Set up Python for website assets
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
cache-dependency-path: website/requirements.txt
- name: Install sharing-image dependencies
run: |
sudo apt-get update
sudo apt-get install --no-install-recommends -y libcairo2 fonts-dejavu-core
python3 -m pip install -r website/requirements.txt
- name: Prepare website assets
run: python3 scripts/prepare-website.py
- name: Check website links and publishing configuration
- name: Check website links, metadata and publishing configuration
run: python3 scripts/verify-website.py
- name: Record the published commit
run: |
+3
View File
@@ -50,6 +50,9 @@ 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.1.0"
".": "6.2.0"
}
+243
View File
@@ -1,5 +1,248 @@
# Changelog
## [6.2.0](https://github.com/morluto/rea/compare/rea-agents-6.1.0...rea-agents-6.2.0) (2026-10-09)
### ⚠ BREAKING CHANGES
Read the [6.1 to 6.2 migration guide](docs/migration-6.2.md) before upgrading. REA's `always-bump-minor` policy includes breaking changes; 6.2 is not a backward-compatibility promise. Preserve original saved files and regenerate missing observations rather than fabricating identities or facts.
* **MCP / Evidence / snapshots / artifacts:** Success results are direct canonical Evidence; read `normalized_result` rather than the old `result`/`evidence` wrapper. Evidence requires nullable `analysis_profile`, session provider aliases are removed, snapshots require complete profile-bound query records, and artifact roles/permissions belong to occurrences. Recreate incompatible saved data; adding fields without recomputing their semantic identity is invalid. ([63bf914](https://github.com/morluto/rea/commit/63bf91405cf97b114a84f337fb1272d98a267999))
* **Process captures:** Legacy v3 conversion is removed. Explicit executable identity and `event_journal` are required; retain old captures and recapture when current comparisons are needed instead of inventing launch identity or event order. ([2a77469](https://github.com/morluto/rea/commit/2a7746998d47446c4db6d4f524e0f183d6803f3f))
* **Cleanup and evaluation:** A one-shot analysis reports cleanup failure while preserving the completed observation or primary diagnostic. Handle `cleanup_incomplete` and retained observations/resources. Configured agent-evaluation fixtures require Evidence-backed correctness, not routing or answer wording alone. ([#1211](https://github.com/morluto/rea/pull/1211), [27a9cea](https://github.com/morluto/rea/commit/27a9ceade7232c1c9a24dc7d454ae060948183a1))
* **JavaScript:** Return-field projections require explicit presence; export-shape changes carry both sides' presence and property inventory coverage. Semantic object operations retain complete `property_path` segments. Regenerate old analysis and distinguish unknown values from unknown presence. ([475a0f8](https://github.com/morluto/rea/commit/475a0f84dac63d710e032bc7f731c1a3bda624ec))
* **Function comparisons:** Remove the unreachable `truncated` dimension state and `summary.truncated` counter; update enum and display consumers while retaining explicit `unknown` facts. ([5510b27](https://github.com/morluto/rea/commit/5510b27d7f863a3df26212fba4a59f8df0da3712))
* **Historical sources:** Unreadable/unknown symlink targets require `target: null`; external targets retain the observed absolute target. Reimport graphs containing display placeholders such as `<outside-root>` and handle nullable targets. ([b7b4333](https://github.com/morluto/rea/commit/b7b433370310c3ffe33d5027d8393151d5e862da))
### Features
* **javascript:** report observed export property presence ([#1160](https://github.com/morluto/rea/issues/1160)) ([45e3edc](https://github.com/morluto/rea/commit/45e3edc17f48954c89d8db86f7313c479bd39344))
* **mcp:** select views of retained analysis results ([#1180](https://github.com/morluto/rea/issues/1180)) ([db675f1](https://github.com/morluto/rea/commit/db675f1d52acda62b6f57b5a8773532f49b259e7))
* **website:** add an Aegis APK code-generation showcase ([0270dd5](https://github.com/morluto/rea/commit/0270dd5739480acced62e045b5890ec0ebde2631))
* **website:** add an Android showcase and clearer project prompts ([6cc817e](https://github.com/morluto/rea/commit/6cc817e8bd377b064b3568ef1eacb27eaefcb4c1))
* **website:** add beginner examples and synchronized publishing ([0fec65d](https://github.com/morluto/rea/commit/0fec65d06b2f5431e704c472e53015d4b3cc967c))
* **website:** add beginner examples and synchronized publishing ([#1203](https://github.com/morluto/rea/issues/1203)) ([0fec65d](https://github.com/morluto/rea/commit/0fec65d06b2f5431e704c472e53015d4b3cc967c))
* **website:** add Discord link beside GitHub badge ([cc39fc5](https://github.com/morluto/rea/commit/cc39fc53b829a349ae199823472990a53014c507))
* **website:** add homepage navigation and faster reading paths ([863674b](https://github.com/morluto/rea/commit/863674b2e92dd7c699768db82d521ccc88b7ebe4))
* **website:** add homepage navigation and faster reading paths ([c94f06d](https://github.com/morluto/rea/commit/c94f06d98f895eb8b6c5ffa5e999ef1d4bb5eea2))
* **website:** add homepage navigation and faster reading paths ([#1213](https://github.com/morluto/rea/issues/1213)) ([863674b](https://github.com/morluto/rea/commit/863674b2e92dd7c699768db82d521ccc88b7ebe4))
* **website:** add search metadata and automatic sitemap ([99f257b](https://github.com/morluto/rea/commit/99f257bbf03c27327e5e0a232fefd3c792acd5bc))
* **website:** add search metadata and automatic sitemap ([b8851cb](https://github.com/morluto/rea/commit/b8851cb977cfdd608798d32bf5877f645b960831)), closes [#1283](https://github.com/morluto/rea/issues/1283)
* **website:** clarify reverse engineering and showcase goals ([e750c26](https://github.com/morluto/rea/commit/e750c262fbf37100369ab25bf8659daa1a2c4cc7))
* **website:** explain REA through a first investigation ([5160d9b](https://github.com/morluto/rea/commit/5160d9b717d0207a357c754bdf72e215e59d19d1))
* **website:** explain REA with Calculator and a playable dinosaur lab ([11b9f03](https://github.com/morluto/rea/commit/11b9f0346cf5413045b41ff552a024e11ba46ccc))
* **website:** lead with the playable dinosaur example ([19c52f5](https://github.com/morluto/rea/commit/19c52f5e3f42fb754fc154bbd16552c5111df0c5))
* **website:** show manual analysis beside a one-prompt workflow ([291bc6f](https://github.com/morluto/rea/commit/291bc6fb70f47d28d23440f03a8a7fa49eb9a391))
### Bug Fixes
* **dev:** accept formatter-ignored staged documentation without failing the commit hook; malformed JSON remains rejected. ([bd2db2f](https://github.com/morluto/rea/commit/bd2db2f0fa495fb08dea443cd16d7683d4b86e6a))
* **release:** publish reviewed release-PR merges from their exact reviewed source. ([c12bad7](https://github.com/morluto/rea/commit/c12bad7f139431fc0db7a8b7c9db1eab95f96dc2))
* **android:** support owned JADX stdio on Windows ([#1276](https://github.com/morluto/rea/issues/1276)) ([636b245](https://github.com/morluto/rea/commit/636b245f1badf2a7ecfa63c6e2378ac2f7673cc3)), closes [#1262](https://github.com/morluto/rea/issues/1262)
* **android:** validate engine archive prerequisites ([#1145](https://github.com/morluto/rea/issues/1145)) ([e9ebfb9](https://github.com/morluto/rea/commit/e9ebfb94eff0699b1ea84bba000c4467fa1cb998))
* **apple:** report unusable dylib-resolution roots as invalid input ([6a1e558](https://github.com/morluto/rea/commit/6a1e558db3ec2b69561e91ea817f1c2065dfc676))
* **apple:** report unusable dylib-resolution roots as invalid input ([24a30fd](https://github.com/morluto/rea/commit/24a30fd91c144b7f2533e5f5de8c91268773519c))
* **artifacts:** keep cancellation and the caller path in extraction refusals ([37025cc](https://github.com/morluto/rea/commit/37025cc09f12d49f46e61eedc3564c710089146b))
* **artifacts:** keep cancellation and the caller path in extraction refusals ([eacf846](https://github.com/morluto/rea/commit/eacf8461dad6bab11a3d5438e6db001a10f33a9b))
* **artifacts:** match lipo arm64e variant names to their FAT slices ([de5bfec](https://github.com/morluto/rea/commit/de5bfec79fe862816350b2deeb42a12c0734d855))
* **artifacts:** match lipo arm64e variant names to their FAT slices ([0b32577](https://github.com/morluto/rea/commit/0b32577a90aef97e36adf2a7af4f3eccd5d82357)), closes [#1168](https://github.com/morluto/rea/issues/1168)
* **artifacts:** name only host-available workflows for non-archive plists ([f4c99ab](https://github.com/morluto/rea/commit/f4c99ab8a53806dd57dc24b3d5a600905bcb4686))
* **artifacts:** name only host-available workflows for non-archive plists ([2295855](https://github.com/morluto/rea/commit/22958558eafdfccf71a7d5bbbcf360574a6b2e3c))
* **artifacts:** recheck failed DMG detaches before reporting cleanup failure ([#1155](https://github.com/morluto/rea/issues/1155)) ([58a91f0](https://github.com/morluto/rea/commit/58a91f03d72162849991caf537ab058ccc918594))
* **artifacts:** refuse unsupported extraction formats before scanning ([8e74c9c](https://github.com/morluto/rea/commit/8e74c9c62fb44556b90f20bb6c12771ac05a18f2))
* **artifacts:** report encrypted archive entries as unsupported extraction ([de43da7](https://github.com/morluto/rea/commit/de43da71ae982dd0f3d7b5ae49c914729db3a6ec))
* **artifacts:** report non-archive keyed-archive selections as unsupported targets ([fef38e4](https://github.com/morluto/rea/commit/fef38e4d22ee859ec43fb591d53e8b9973c69958))
* **artifacts:** report non-archive keyed-archive selections as unsupported targets ([8c5088c](https://github.com/morluto/rea/commit/8c5088c8cdf32a9f96f6c479c6e70a71fa360168))
* **artifacts:** report wrong target kinds as unsupported targets ([b34bdcd](https://github.com/morluto/rea/commit/b34bdcdb2877be394c59c9cbf70269dc1841ff78))
* **artifacts:** report wrong target kinds as unsupported targets ([637bcb5](https://github.com/morluto/rea/commit/637bcb5e4182b6c60e922b18688642d8e1008e90))
* **browser:** classify export-web-scripts capture selection failures by cause ([ddaa183](https://github.com/morluto/rea/commit/ddaa18301ab9c67e7a6f7ce66076075e8a4d5017))
* **browser:** classify export-web-scripts capture selection failures by cause ([8c5e34c](https://github.com/morluto/rea/commit/8c5e34cd379392ca40f651ca5eeb6a947b44aa33))
* **browser:** report Windows HAR heap exhaustion ([#1147](https://github.com/morluto/rea/issues/1147)) ([1393507](https://github.com/morluto/rea/commit/13935077ba039f6e342f9c11ab5eea935a335ae6))
* **browser:** retain scenario observations across failures ([614e0c2](https://github.com/morluto/rea/commit/614e0c2e74819bb5e3ef3d2e16ff31ee6cab76ae))
* **browser:** retain scenario resources until cleanup succeeds ([88640f6](https://github.com/morluto/rea/commit/88640f60fe566edbe2320688df62c7ef46c4c61c))
* **ci:** pass selected environment to Windows Ghidra fixture ([#1281](https://github.com/morluto/rea/issues/1281)) ([f085589](https://github.com/morluto/rea/commit/f085589957922eaf421db814399e88e415712050))
* **cli:** advertise defaulted inputs as optional in --schema and help ([#1175](https://github.com/morluto/rea/issues/1175)) ([b4927a0](https://github.com/morluto/rea/commit/b4927a0a4fa7e8935b101d214b396bfa78208281)), closes [#1169](https://github.com/morluto/rea/issues/1169)
* **cli:** cancel routed JavaScript analysis in rea analyze ([#1156](https://github.com/morluto/rea/issues/1156)) ([089bbf5](https://github.com/morluto/rea/commit/089bbf58f3561e3f465c544bbcf8c307287b1256))
* **cli:** complete cleanup after relayed interrupts ([#1202](https://github.com/morluto/rea/issues/1202)) ([911152a](https://github.com/morluto/rea/commit/911152a04d3aeb0d361141a8d54959009b269e49))
* **cli:** list the read failure cause for an unreadable JSON input file ([db0afce](https://github.com/morluto/rea/commit/db0afce5f344268271c59acee04e96f986c5619a))
* **cli:** list the read failure cause for an unreadable JSON input file ([6020df5](https://github.com/morluto/rea/commit/6020df53d9cd44845cf3b6b416db2d7802925578))
* **cli:** resolve xrefs symbol selectors after address validation ([#1209](https://github.com/morluto/rea/issues/1209)) ([7e9c911](https://github.com/morluto/rea/commit/7e9c91199ecd133d26a51cb33b6e63e1f942df7e))
* **config:** name the rejected setting and its constraint in configuration errors ([c3acc34](https://github.com/morluto/rea/commit/c3acc34475ca22387ea674839acfa1891b0b9f66))
* **config:** name the rejected setting and its constraint in configuration errors ([0a0244d](https://github.com/morluto/rea/commit/0a0244db4212bc1f59dec4b925fa90b3b473136a)), closes [#1184](https://github.com/morluto/rea/issues/1184)
* **doctor:** find the Homebrew Hopper cask without writing Homebrew caches ([98b07d0](https://github.com/morluto/rea/commit/98b07d07ddd20bea2c3bdf8d55ac2776a263bcf4))
* **doctor:** find the Homebrew Hopper cask without writing Homebrew caches ([a503376](https://github.com/morluto/rea/commit/a503376b2f9d9ba14d1602e58e0e93059e9d9181)), closes [#1165](https://github.com/morluto/rea/issues/1165)
* **domain:** report target read-permission denials as access_denied ([f36c10f](https://github.com/morluto/rea/commit/f36c10f9e8dc45a298d668e608f8e945bde487cf))
* **domain:** report target read-permission denials as access_denied ([d0ff57a](https://github.com/morluto/rea/commit/d0ff57a45c8da8938e7ae16d00bfd5ec63310af3)), closes [#1188](https://github.com/morluto/rea/issues/1188)
* **electron:** resolve effective preload after option overrides ([#1286](https://github.com/morluto/rea/issues/1286)) ([411498d](https://github.com/morluto/rea/commit/411498d45f860eba2fbc9d7051af20237b7a4cb1))
* emit v-mode-compatible patterns for tool schemas ([#1220](https://github.com/morluto/rea/issues/1220)) ([f0da05f](https://github.com/morluto/rea/commit/f0da05f67fff1411cf2f7a48a752ee7b348c71ff))
* **errors:** report caller-selected evidence path failures as invalid input ([d4a598e](https://github.com/morluto/rea/commit/d4a598e93649607dd293a9c81bbf79c211bc8dd4))
* **errors:** report caller-selected evidence path failures as invalid input ([db177c9](https://github.com/morluto/rea/commit/db177c91b83adb0c3edc4f9f6f06e31ac7523d53))
* **evidence:** cancel exports before atomic publication ([16624f1](https://github.com/morluto/rea/commit/16624f1324e156d14025b2c346596c9781c07488))
* **ghidra:** classify modern Swift manglings in swift type analysis ([#1208](https://github.com/morluto/rea/issues/1208)) ([6935dc7](https://github.com/morluto/rea/commit/6935dc7fe1258b340bb9cdfb8cecae66ef7bc0d4))
* **ghidra:** do not run Java from a relative JAVA_HOME in doctor and setup ([04a516b](https://github.com/morluto/rea/commit/04a516b1e369f7e57e08d0653cc08292ac497e07))
* **ghidra:** do not run Java from a relative JAVA_HOME in doctor and setup ([c3f1698](https://github.com/morluto/rea/commit/c3f169823f690371ecfdd2d1b3b07a5749438cf1))
* **ghidra:** keep and name a runtime directory that close could not remove ([5b769d3](https://github.com/morluto/rea/commit/5b769d3a781c645ed8885e4fabf01a00f0bbc89b))
* **ghidra:** keep and name a runtime directory that close could not remove ([b79d379](https://github.com/morluto/rea/commit/b79d379c25bcc8ed61571c41a94b912b5704a067)), closes [#1190](https://github.com/morluto/rea/issues/1190)
* **ghidra:** keep the NativeAOT target refusal's own recovery advice ([f198993](https://github.com/morluto/rea/commit/f198993f418e6aaa9755e7e6413abcb27c6cd684))
* **ghidra:** preserve decoded return flow evidence ([b189c3c](https://github.com/morluto/rea/commit/b189c3ceb175dd9fa089eada9e9ca54e76307cb3))
* **ghidra:** preserve decoded return flow evidence ([bcc8a77](https://github.com/morluto/rea/commit/bcc8a773e2a714e4899755b786a4fef1539bbace))
* **ghidra:** reject relative installation and Java paths in doctor and setup ([8998438](https://github.com/morluto/rea/commit/8998438c23aa19e68f4ffa7574c5d341aa419165))
* **ghidra:** reject relative installation and Java paths in doctor and setup ([5356856](https://github.com/morluto/rea/commit/53568568bf32b36274ba3a532f1e2cfb2daed271)), closes [#1183](https://github.com/morluto/rea/issues/1183)
* **ghidra:** report NativeAOT host and target refusals as unsupported, not failures ([cfb49eb](https://github.com/morluto/rea/commit/cfb49eb8a88fc51f460907024c51e05a2f272502))
* **ghidra:** report NativeAOT host and target refusals as unsupported, not failures ([42c276a](https://github.com/morluto/rea/commit/42c276af1ec02473a7b6700a31292aae9e53d53e)), closes [#1185](https://github.com/morluto/rea/issues/1185)
* **ghidra:** resolve secondary symbols at function entries ([#1195](https://github.com/morluto/rea/issues/1195)) ([6fbdbd6](https://github.com/morluto/rea/commit/6fbdbd682038d012b04f0fe92c470cf1789fb101))
* **ghidra:** stop dereferencing the removed error payload in address verification ([b80bd2d](https://github.com/morluto/rea/commit/b80bd2db966731fd3cfddfff7e92704053dbe955))
* **ghidra:** stop dereferencing the removed error payload in address verification ([59fbaa2](https://github.com/morluto/rea/commit/59fbaa2d33359216534407f99f49705ac04a3426))
* **ghidra:** use dot-free private session paths ([c3fc6c3](https://github.com/morluto/rea/commit/c3fc6c3aedc202e8ade015c4039a63e9f91cac6d))
* **hopper:** keep the OS error when the FAT64 source cannot be reopened ([9f5dc52](https://github.com/morluto/rea/commit/9f5dc52be8420fb5e874595d63312bf80d07a3a2))
* **hopper:** reserve Linux demo application before launch ([#1158](https://github.com/morluto/rea/issues/1158)) ([33d5ae0](https://github.com/morluto/rea/commit/33d5ae0b52b8684634ecf14d4e9469968c69d462))
* **javascript:** allow cancellation during semantic graph commitment ([#1218](https://github.com/morluto/rea/issues/1218)) ([9405bd6](https://github.com/morluto/rea/commit/9405bd660def9946afaa5fcc1d99921a816911bf))
* **javascript:** classify application input selection failures by cause ([fe5291e](https://github.com/morluto/rea/commit/fe5291e9166977a6ebbfa6ffcf7850af57192b34))
* **javascript:** classify application input selection failures by cause ([0a14bbb](https://github.com/morluto/rea/commit/0a14bbbf05e0e61c98e06326fe7136e52fa4fb3c))
* **javascript:** keep Wakaru version stderr out of the refusal message ([6489d58](https://github.com/morluto/rea/commit/6489d58851d055fd7ce621000d960bb42a0e4ce0))
* **javascript:** match source-map originals that name parent directories ([#1215](https://github.com/morluto/rea/issues/1215)) ([889fb37](https://github.com/morluto/rea/commit/889fb374df0f8986f49e9d61b0fefd536327f781))
* **javascript:** omit self-referential static references with disclosure ([#1157](https://github.com/morluto/rea/issues/1157)) ([b620e2a](https://github.com/morluto/rea/commit/b620e2a2bdcf3683b494d137bc409813735d5572))
* **javascript:** preserve alias and container uncertainty ([73e98b2](https://github.com/morluto/rea/commit/73e98b2516dd792ea7b510aeed6b71f384bea21d))
* **javascript:** preserve graphs with async chunk self-references ([#1152](https://github.com/morluto/rea/issues/1152)) ([7220166](https://github.com/morluto/rea/commit/72201668ed88c2cc822a07c08cf35a1e5ba88f35))
* **javascript:** preserve property paths and uncertain slot presence ([475a0f8](https://github.com/morluto/rea/commit/475a0f84dac63d710e032bc7f731c1a3bda624ec))
* **javascript:** reject encoded separators joined by HTML URL whitespace ([2ddd945](https://github.com/morluto/rea/commit/2ddd9456cd405f081784a4b3cb02cb77cd90fbd6))
* **javascript:** reject encoded separators joined by HTML URL whitespace ([fb57c86](https://github.com/morluto/rea/commit/fb57c8643fb8b4733cbac9090ef58fd13a3b5347))
* **javascript:** report located sources with differing digests as modified ([#1216](https://github.com/morluto/rea/issues/1216)) ([01174c4](https://github.com/morluto/rea/commit/01174c4a81fea109ae712ea1a746a83996f928ac))
* **javascript:** resolve HTML script URLs after URL whitespace and percent decoding ([d601b80](https://github.com/morluto/rea/commit/d601b8016eaeb2955cea4e4c0aca10d17e7d12a7))
* **javascript:** resolve HTML script URLs after URL whitespace and percent decoding ([502528c](https://github.com/morluto/rea/commit/502528c58df54c7b34eefdb5837f36e58f473405)), closes [#1236](https://github.com/morluto/rea/issues/1236)
* **javascript:** resolve package exports targets only to the exact file ([#1181](https://github.com/morluto/rea/issues/1181)) ([2a620c0](https://github.com/morluto/rea/commit/2a620c06cdcaa32844c4a80aeb3df1793f1ce09e)), closes [#1174](https://github.com/morluto/rea/issues/1174)
* keep the Ghidra session root free of dot-prefixed path elements ([a8b8288](https://github.com/morluto/rea/commit/a8b8288396cf2e053db768ef03f4db756aa8f71e))
* **managed:** preserve literal library names during native matching ([81b70bc](https://github.com/morluto/rea/commit/81b70bc358031686c9d7afd049f21719c67a9525))
* **managed:** separate metadata and signature completeness ([2f02eff](https://github.com/morluto/rea/commit/2f02eff9887fc715c33f5d8770a6e6191ba70ca9))
* **mcp:** advertise input patterns without look-around ([1f099a6](https://github.com/morluto/rea/commit/1f099a6fc00b08fb0283dcabc5a238025d4c4c8b))
* **mcp:** advertise input patterns without look-around ([a8701ea](https://github.com/morluto/rea/commit/a8701eaf489692449739d5b2490502b1a14951c5)), closes [#1282](https://github.com/morluto/rea/issues/1282)
* **mcp:** deliver tool errors within advertised schema contracts ([da606ff](https://github.com/morluto/rea/commit/da606ff77718d9a3b10426fcf68c098138e6dcfa)), closes [#1261](https://github.com/morluto/rea/issues/1261)
* **mcp:** deliver typed errors without invalid structured output ([#1277](https://github.com/morluto/rea/issues/1277)) ([7e96f25](https://github.com/morluto/rea/commit/7e96f25f8b1380a13a086a68f8d5a70380468151))
* **mcp:** emit portable Unicode NUL patterns ([#1230](https://github.com/morluto/rea/issues/1230)) ([aa5750c](https://github.com/morluto/rea/commit/aa5750c263632d98d626c5ca3b985bb1519d2395))
* **mcp:** measure tool errors against their text-only delivery ([fe34ec2](https://github.com/morluto/rea/commit/fe34ec2ef3e2968eeb06f64a1a0fdb8b825f56c4))
* **mcp:** measure tool errors against their text-only delivery ([7b51121](https://github.com/morluto/rea/commit/7b51121f7cee21a5938f10ac3b5762e0bb41a58d))
* **mcp:** only recommend exporting retained evidence ([5f749c2](https://github.com/morluto/rea/commit/5f749c2096af2617d4b6b387ff19c13001af8e54))
* name host and tool requirements in optional adapter refusals ([68cb02f](https://github.com/morluto/rea/commit/68cb02fe8b123b9a0778c0065c13a15bcf0e577d))
* name host and tool requirements in optional adapter refusals ([170d0bf](https://github.com/morluto/rea/commit/170d0bf9e2d45b2512ad556c02f31bdc81ea03ca))
* **native:** distinguish batch selectors from resolved entries ([13c3b68](https://github.com/morluto/rea/commit/13c3b68730df80330fd867d921dea38770d5f880))
* **native:** distinguish batch selectors from resolved entries ([bfaa5a6](https://github.com/morluto/rea/commit/bfaa5a6c2c0ddc9e6fcf16cd11ad06f24db174a2))
* **native:** name the CLI flag that selects another plist ([9b69503](https://github.com/morluto/rea/commit/9b6950373cfaa1a2c0d28287bfadd4a421afb9e9))
* **native:** name the plist selection that works when Info.plist is missing ([5612496](https://github.com/morluto/rea/commit/5612496f6d34a462dcd0fb69fa38337592e2f72d))
* **native:** name the plist selection that works when Info.plist is missing ([a6fdf42](https://github.com/morluto/rea/commit/a6fdf426bd98c8cf7b7a7f589208d3193784062f))
* **native:** refuse truncated executable headers at target admission ([d39a450](https://github.com/morluto/rea/commit/d39a450bdfac425962c3a6a6f47b45600057a230))
* **native:** refuse truncated executable headers at target admission ([0b04d38](https://github.com/morluto/rea/commit/0b04d384cca4f22f271d60bcfe67f79cab89c8de)), closes [#1189](https://github.com/morluto/rea/issues/1189)
* **process:** apply one timeout rule to both execFileOutput modes ([3d32819](https://github.com/morluto/rea/commit/3d328193d69a63a355644c51bfb70f524d99bca9))
* **process:** await cleanup on CLI and MCP cancellation ([f6fe575](https://github.com/morluto/rea/commit/f6fe575094658e99ced78d45f385633fbfaaae32))
* **process:** keep ports before a chunk-final period ([184ed97](https://github.com/morluto/rea/commit/184ed970b9021552a5454ced20d202d10cabcbb8))
* **process:** localize capture truncation diagnostics ([#1284](https://github.com/morluto/rea/issues/1284)) ([33bf585](https://github.com/morluto/rea/commit/33bf585185ffcaea63d658e2d3145c900a680485))
* **process:** normalize ports that end a sentence ([098d769](https://github.com/morluto/rea/commit/098d76900effdbc682b0f97bcde7f8711380ecea))
* **process:** normalize ports that end a sentence ([db98a51](https://github.com/morluto/rea/commit/db98a51dd1750b09b3905e18efac526f7ebf90f7))
* **process:** preserve numeric output and original PTY evidence ([c5c75c3](https://github.com/morluto/rea/commit/c5c75c38c3a4ff0153513b8f8ad600d8a5bacc09))
* **process:** preserve numeric output and original PTY evidence ([00d8e44](https://github.com/morluto/rea/commit/00d8e44cbf82caa51709c2e0857fbaa69ea329b7))
* **process:** preserve unknown filesystem absence in partial captures ([c8544b1](https://github.com/morluto/rea/commit/c8544b1574a32838da6d4ed40f2b72582edb60dd))
* **process:** refuse malformed timeouts for stoppable commands ([136d4aa](https://github.com/morluto/rea/commit/136d4aab18d822f0f908096b8acfac556ef1873b))
* **process:** refuse malformed timeouts for stoppable commands ([59c91c8](https://github.com/morluto/rea/commit/59c91c88fe88210b4b22b9f1365a16567513da4a))
* **process:** require current captures and preserve cleanup failures ([2a77469](https://github.com/morluto/rea/commit/2a7746998d47446c4db6d4f524e0f183d6803f3f))
* **process:** retain known events from incomplete captures ([5eb0049](https://github.com/morluto/rea/commit/5eb0049a9f502b404f30e48c3fe3430a942bb0b5))
* **process:** stop cancelled Swift helper builds before removing their files ([cf41561](https://github.com/morluto/rea/commit/cf415619cadcfcb18cda59a54b26c9d164f750a4))
* **process:** stop cancelled Swift helper builds before removing their files ([c717204](https://github.com/morluto/rea/commit/c7172047951e7dbb9df58b57ff1b43ac9bd47909)), closes [#1173](https://github.com/morluto/rea/issues/1173)
* **process:** stop timed-out commands with the caller's stop signal ([4b38d18](https://github.com/morluto/rea/commit/4b38d1899566089d19b9fd137202f7d86b62fc6f))
* **process:** stop timed-out commands with the caller's stop signal ([063de50](https://github.com/morluto/rea/commit/063de5014f19e85c38b6bec5984de4f38e71adaa))
* **process:** treat non-ASCII letters as port continuations ([ad813e4](https://github.com/morluto/rea/commit/ad813e473b094ab0d3ab30798b7d252e2f4beb94))
* **process:** validate terminal byte totals against retained frames ([69833fb](https://github.com/morluto/rea/commit/69833fb0d0fe9b320b5745a992822342f2c54a92))
* **process:** validate terminal byte totals against retained frames ([39eb058](https://github.com/morluto/rea/commit/39eb0587c95ad028bb961c539ad1f326a1e8d81c))
* **providers:** preserve observations and primary cleanup failures ([0696bde](https://github.com/morluto/rea/commit/0696bde2660bb5d6538d8c01da15aa023312aa9f))
* **reference:** represent unreadable symlink targets explicitly ([b7b4333](https://github.com/morluto/rea/commit/b7b433370310c3ffe33d5027d8393151d5e862da))
* restore caching, preserve failures, and verify fixture claims ([#1211](https://github.com/morluto/rea/issues/1211)) ([27a9cea](https://github.com/morluto/rea/commit/27a9ceade7232c1c9a24dc7d454ae060948183a1))
* **server:** report 2025-era client identity and features in binary_session ([e6abaef](https://github.com/morluto/rea/commit/e6abaefbbc2bd81a6be34cbe71b87be801158c16))
* **server:** report 2025-era client identity and features in binary_session ([b8312fa](https://github.com/morluto/rea/commit/b8312fa8d184ba9f422d1f5592fb41e49811544a)), closes [#1167](https://github.com/morluto/rea/issues/1167)
* **session:** require an open target for session-bound native and artifact tools ([d8e314c](https://github.com/morluto/rea/commit/d8e314cb24e7a4a494347e45474234e0aad74ad1))
* **session:** require an open target for session-bound native and artifact tools ([cf77d07](https://github.com/morluto/rea/commit/cf77d073fc06ab95ce4181c30d68b1346e990b64)), closes [#1166](https://github.com/morluto/rea/issues/1166)
* **session:** retain failed provider cleanup for lifecycle retries ([#1260](https://github.com/morluto/rea/issues/1260)) ([24a3aa0](https://github.com/morluto/rea/commit/24a3aa08d1f69e25b441f086300ba3082d860a35))
* **setup:** keep Codex TOML comments when adding or removing REA ([f5539cf](https://github.com/morluto/rea/commit/f5539cfc2f6c0daf6c275fc97f3351e2cf89b2c3))
* **setup:** keep Codex TOML comments when adding or removing REA ([e9c48a6](https://github.com/morluto/rea/commit/e9c48a61e4c2c05c5b77ef4a815c5b2a65a9337e)), closes [#1163](https://github.com/morluto/rea/issues/1163)
* **setup:** preserve initial backups and Codex comments ([#1204](https://github.com/morluto/rea/issues/1204)) ([6051cb2](https://github.com/morluto/rea/commit/6051cb2c0c723d88dc43ee5ef8a96971bf84d49e))
* **setup:** stop remaining uninstall removals after a client fails ([fd975c7](https://github.com/morluto/rea/commit/fd975c75fa353840e6e887232ca68496cf8705ea))
* **setup:** stop uninstall before removals when a client config is unsafe ([4fb2f3e](https://github.com/morluto/rea/commit/4fb2f3e6ed0233505eada3cddeeff0ea0bfc6fe5))
* **setup:** stop uninstall before removals when a client config is unsafe ([5cc841d](https://github.com/morluto/rea/commit/5cc841d5c154f692bdc257e106cc4b212dc19dc0)), closes [#1164](https://github.com/morluto/rea/issues/1164)
* **snapshot:** report query workflow and Evidence counts separately ([04aef6d](https://github.com/morluto/rea/commit/04aef6df331634238cfc61f320ba4a4e720750a4))
* **snapshot:** report query workflow and Evidence counts separately ([0e77953](https://github.com/morluto/rea/commit/0e779533a9108a550b4d10dcb3920f91c7d48989))
* **website:** clarify Android host and JDK requirements ([5691606](https://github.com/morluto/rea/commit/5691606aa948bddc641e3ecbccf5375a12b65924))
* **website:** identify publication verifier requests ([e6905c1](https://github.com/morluto/rea/commit/e6905c18e09c41e04525fb1a84575483df789204))
* **windows:** retain runtime authority after cleanup failure ([af7a439](https://github.com/morluto/rea/commit/af7a43963351c655eb86197bf52ae798a04e9c22))
* **windows:** retain unsettled jobs for cleanup retry ([76b214e](https://github.com/morluto/rea/commit/76b214e45fa05fe723e216894327185b9b4e6f53))
### Performance Improvements
* **mcp:** share repeated output schema definitions. ([441813c](https://github.com/morluto/rea/commit/441813c0708dbee82ef554c91e973ae02964bf2e))
* **hopper:** resolve string search records once per query ([#1229](https://github.com/morluto/rea/issues/1229)) ([9426c3f](https://github.com/morluto/rea/commit/9426c3f1bbb94711d8b33097e604b9b8ed853b5e))
* **mcp:** reuse input schemas without losing caller guidance ([#1162](https://github.com/morluto/rea/issues/1162)) ([e1afbc1](https://github.com/morluto/rea/commit/e1afbc1914cc7ff90265313e942f34ea4a58a4fc))
* **snapshot:** avoid redundant result copies and serialization ([552634d](https://github.com/morluto/rea/commit/552634d634655a722b2204cfbc4b29f6d442c294))
* **snapshot:** avoid redundant result copies and serialization ([569d2bc](https://github.com/morluto/rea/commit/569d2bc6b9f600114267a4c69f8400393ea1bc2f))
### Code Refactoring
* **domain:** remove obsolete comparison representations ([5510b27](https://github.com/morluto/rea/commit/5510b27d7f863a3df26212fba4a59f8df0da3712))
* enforce canonical evidence and provider boundaries ([63bf914](https://github.com/morluto/rea/commit/63bf91405cf97b114a84f337fb1272d98a267999))
* remove forwarding exports and redundant aliases ([ad59531](https://github.com/morluto/rea/commit/ad59531a960a0c51ef6857fa1391270dda0fa7a4))
* **schemas:** share portable identifier and base64 patterns ([#1227](https://github.com/morluto/rea/issues/1227)) ([954badb](https://github.com/morluto/rea/commit/954badbaaadd24a210190ce3015e40c23d327567))
### Documentation
* add French README translation ([84094af](https://github.com/morluto/rea/commit/84094af54a4e0a6247445e59abff27bc4eca361b))
* add French README translation ([9664b2b](https://github.com/morluto/rea/commit/9664b2b101fe4ac334b2911f9210b50d58d90a89))
* celebrate 30,000 GitHub stars 🎉 ([6987cb0](https://github.com/morluto/rea/commit/6987cb066a5aa5e51480bb06a93a758e6f67cd1e))
* celebrate 30,000 GitHub stars 🎉 ([beca997](https://github.com/morluto/rea/commit/beca9977eb2b18fc7b9844cff0ca70004b6ff21a))
* clarify REA has no affiliated cryptocurrency or token ([#1214](https://github.com/morluto/rea/issues/1214)) ([c2d8146](https://github.com/morluto/rea/commit/c2d8146f94c7533eb87681c2405e93bdfef21b59))
* explain 6.2 contract and saved-data migration ([#1304](https://github.com/morluto/rea/issues/1304)) ([c267e99](https://github.com/morluto/rea/commit/c267e9922751c03df312655ebd623de1b480215c))
* **ghidra:** explain supported heap and CPU controls ([#1278](https://github.com/morluto/rea/issues/1278)) ([0c6852b](https://github.com/morluto/rea/commit/0c6852b392b34319a6fd2c6dffcac10cee51ab94)), closes [#1255](https://github.com/morluto/rea/issues/1255)
* keep the 30k milestone in Star History ([#1217](https://github.com/morluto/rea/issues/1217)) ([c399670](https://github.com/morluto/rea/commit/c399670653884673158f314ed38260aef74da593))
* require focused brittleness cleanup and executable examples ([d6832c3](https://github.com/morluto/rea/commit/d6832c3632847cf3e62d4082b4b8b8d9af554633))
* thank feature request contributors ([9db1fd0](https://github.com/morluto/rea/commit/9db1fd0bd7b17ed1dd21b9c72dabfa4ec6c23a3f))
* **website:** continue the Calculator example with reconstruction ([4578647](https://github.com/morluto/rea/commit/4578647c59a7487bce597a65be6eab8be0412e84))
* **website:** continue the Calculator example with reconstruction ([18a1e8c](https://github.com/morluto/rea/commit/18a1e8cd0efe14c06fd14aabe0c318d2ae390f78))
* **website:** explain SEO maintenance and indexing follow-up ([bb8ff2c](https://github.com/morluto/rea/commit/bb8ff2cfba638ee3a77f951c01e98340c757f525))
* **website:** introduce the worked homepage examples ([f6f5c16](https://github.com/morluto/rea/commit/f6f5c160979624cdcb33928f1a77cf000de403d4))
* **website:** invite deeper projects and reader feedback ([3fb9ffa](https://github.com/morluto/rea/commit/3fb9ffaa4d67685412873cbd3101c64fabbeaff3))
### Tests
* **apple:** verify compiled Mach-O root occurrence facts ([#1300](https://github.com/morluto/rea/issues/1300)) ([4bb4c6f](https://github.com/morluto/rea/commit/4bb4c6fe6d34525c7665a9d38b584b026dd5cdd9))
* **artifacts:** assert asset-catalog target refusal only on macOS ([44d5b4b](https://github.com/morluto/rea/commit/44d5b4b25e0831769c1bd4e5426ef498d0bd3c74))
* **artifacts:** pin per-occurrence roles for direct Mach-O roots and shared bytes ([701d90a](https://github.com/morluto/rea/commit/701d90aba48aaf4f23e1a73e46ebf9968fb85293))
* **artifacts:** pin per-occurrence roles for direct Mach-O roots and shared bytes ([b9e29c5](https://github.com/morluto/rea/commit/b9e29c52d483704c64cd88f87eaa117066288602)), closes [#1234](https://github.com/morluto/rea/issues/1234) [#1235](https://github.com/morluto/rea/issues/1235)
* **artifacts:** read /bin/ls slice layout from its FAT table ([#1223](https://github.com/morluto/rea/issues/1223)) ([c89b3a0](https://github.com/morluto/rea/commit/c89b3a00dce8df36ddb696b59c80e1adc6aceac1))
* **artifacts:** verify compiled Mach-O roles and permissions ([4bb4c6f](https://github.com/morluto/rea/commit/4bb4c6fe6d34525c7665a9d38b584b026dd5cdd9))
* consolidate redundant assertions into boundary workflows ([b71a591](https://github.com/morluto/rea/commit/b71a591a60e3fa5333889e3cf97e05f4ae03c686))
* **ghidra:** preserve selected verification resource limits ([1c3766a](https://github.com/morluto/rea/commit/1c3766a8bcc8a2b6af679cdaa92a73fec42b3372))
* **ghidra:** size oversized annotations for canonical delivery ([1e34197](https://github.com/morluto/rea/commit/1e341973cfad1dbf06755f3a086ffc23d42a723c))
* **ghidra:** use host-native absolute session root fixtures ([1e83d27](https://github.com/morluto/rea/commit/1e83d270c2098b1bcb59591698a257a05d2296fc))
* **javascript:** expect the invalid input_path code in the streamed CLI failure test ([d186ca7](https://github.com/morluto/rea/commit/d186ca7e41928fc2748de2631f5cb43115776571))
* **mcp:** read bundle failures from public error content ([69c3b32](https://github.com/morluto/rea/commit/69c3b32a21c90ea96194fb695f7b58992e8b3714))
* **mcp:** read typed errors in canonical Evidence regressions ([fd46cff](https://github.com/morluto/rea/commit/fd46cff017f0110aaab1216764cf01a71b341766))
* **mcp:** validate public error diagnostics in acceptance workflows ([09058eb](https://github.com/morluto/rea/commit/09058ebc4a68c76d865a92dbddb65811ba8344f7))
* **native:** assert batch identity in production registration ([933fcb3](https://github.com/morluto/rea/commit/933fcb33c1fa1eae109228b89c1200abb7745eb8))
* **native:** build the .app fixture executable with a complete Mach-O header ([facaae8](https://github.com/morluto/rea/commit/facaae865af243656ab8f557aeb42f5909748f2b))
* **process:** check malformed capture choices concurrently ([6020a0b](https://github.com/morluto/rea/commit/6020a0b3683ce16e1441963473605f870c247e29))
* **process:** check malformed capture choices concurrently ([48de11c](https://github.com/morluto/rea/commit/48de11ce2e5af9810fc97d1c2f89df56bf3a3d4d))
* remove duplicate workflow examples and self assertions ([d6d1cd2](https://github.com/morluto/rea/commit/d6d1cd248506015b59a2abdbbb84e3dbd02388fc))
### Continuous Integration
* **website:** allow explicitly requested Pages-only publication ([796b65e](https://github.com/morluto/rea/commit/796b65e1cd15f3cf819396716320db007655ee3f))
* **website:** allow explicitly requested Pages-only publication ([864160d](https://github.com/morluto/rea/commit/864160d8858fad3c0afebff81c31b090a1ebf823))
* **website:** allow explicitly requested Pages-only publication ([#1205](https://github.com/morluto/rea/issues/1205)) ([796b65e](https://github.com/morluto/rea/commit/796b65e1cd15f3cf819396716320db007655ee3f))
* **website:** publish Cloudflare and Pages from one artifact ([761881c](https://github.com/morluto/rea/commit/761881cec33b18d7228bae5e2093e5d44b96ceb1))
## [6.1.0](https://github.com/morluto/rea/compare/rea-agents-6.0.0...rea-agents-6.1.0) (2026-10-09)
+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.1.0", "mcp"]
"args": ["-y", "rea-agents@6.2.0", "mcp"]
}
}
}
+5 -2
View File
@@ -294,8 +294,11 @@ do not select a later entry. Rejected references retain the package metadata
path, original target, failed constraint, and importing source location.
HTML script references resolve to exact inventoried files after applying the
document base and query/fragment rules. CommonJS module lookups retain extension
and directory resolution.
document base and query/fragment rules. As in a browser, the script URL and base
href are read without surrounding whitespace or embedded tabs and newlines, and
their percent-encoded path bytes are decoded; encoded dot and separator bytes
are still rejected. CommonJS module lookups retain extension and directory
resolution.
Unresolved HTML references retain their declaration, source range, and resolution
reason in the renderer observations.
HTML script source ranges follow the HTML parser across LF, CRLF, and bare CR
+14 -1
View File
@@ -77,6 +77,15 @@ inventories, and do not claim that no short-lived descendant existed.
## Progress and cancellation
Tool execution failures return `isError: true` and the complete canonical
`{ "error": ... }` diagnostic as JSON in text `content`. Read that diagnostic
for the error code, target details, partial observations, and remediation.
Failures omit `structuredContent`, since each advertised `outputSchema`
describes successful results. Successful replies retain their schema-validated
structured data and matching text projection. Oversized errors are retained
as Evidence before delivery of a bounded diagnostic with an export reference;
if retention is unavailable or fails, the diagnostic reports that reason.
REA accepts ordinary `tools/call` progress tokens. Updates are monotonic,
rate-bounded to at most one intermediate update per 100 ms, and always allow a
terminal update. Unknown totals are omitted; REA does not fabricate percentages.
@@ -179,6 +188,9 @@ 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
@@ -192,7 +204,8 @@ 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. Response budget settings are captured at startup;
room for the JSON-RPC envelope. Tool errors carry only their text
representation, so only that text counts against the budget. Response budget settings are captured at startup;
restart or recreate the server to apply changes. If a result cannot fit, REA returns
`resource_constraint` with `details.resource: "transport"` before constructing
a document-sized string. Analysis Evidence remains complete in the current
+157
View File
@@ -0,0 +1,157 @@
# Upgrading from REA 6.1 to 6.2
REA 6.2 includes stability fixes and changes to public result and saved-data
contracts. The repository uses `always-bump-minor`, including breaking releases;
the minor version increment does not imply backward compatibility. Refresh cached
MCP tool definitions after upgrading and adapt custom consumers using this guide.
## MCP results and Evidence
Evidence-producing tools now return the complete Evidence record directly in
`structuredContent` and JSON text. The old `{ result, evidence_id, evidence }`
wrapper is gone.
For a successful Evidence-producing call, change:
```javascript
// 6.1
const value = reply.structuredContent.result;
const evidence = reply.structuredContent.evidence;
// 6.2
const evidence = reply.structuredContent;
const value = evidence.normalized_result;
const evidenceId = evidence.evidence_id;
```
Check `isError` before reading success fields. Error results have `isError: true`
and carry their typed JSON diagnostic in a text content block; they omit
`structuredContent` so they cannot violate the advertised success output schema.
```javascript
if (reply.isError === true) {
const text = reply.content.find((part) => part.type === "text");
if (text === undefined) throw new Error("REA error has no text diagnostic");
const diagnostic = JSON.parse(text.text);
// Handle diagnostic.code, diagnostic.message and diagnostic.details.
}
```
Lifecycle and other non-Evidence tools still use their own advertised output
contracts. Do not assume every successful tool returns Evidence. Consult
[`tools/list` and the result guide](mcp-contracts.md#tool-results).
A transport `resource_constraint` error can include a retained
`details.reported_limits.evidence_reference`. Use `inspect_analysis_view` or
`export_evidence_bundle` on the same connection to consume the complete retained
Evidence. Response-budget settings are captured when the server starts; recreate
the server to change them, and keep the client's receive budget aligned. Raising
the server budget does not change the client's limit or analysis coverage.
Every Evidence record now requires `analysis_profile`, either its observed
profile object or `null`. The field participates in Evidence identity. Records
that omitted it cannot be migrated by inserting `null` while retaining the old
`evidence_id`. Keep the original files and regenerate affected observations with
6.2; use the new returned identities when linking or comparing them. Existing
records that already satisfy the complete canonical contract remain usable.
The `binary_session` result removes the redundant top-level `provider` and
`providers` aliases. Read `analysis_provider_binding`,
`analysis_provider_candidates` and per-operation `capabilities` for selection
and availability. Select a deep provider through `open_binary.provider_id`
or CLI `--provider`, using the advertised values.
These changes establish one complete result representation and explicit
provider/profile ownership rather than accepting incomplete producer records.
See [63bf914](https://github.com/morluto/rea/commit/63bf91405cf97b114a84f337fb1272d98a267999)
and [#1277](https://github.com/morluto/rea/pull/1277).
## Saved snapshots and artifact inventories
Analysis snapshots require `workflow_entries` and a corresponding profile-bound
Evidence record for every cached query. Import rejects incomplete bindings
instead of silently discarding them. Preserve an incompatible snapshot and
choose a new output path when rerunning analysis, for example:
```bash
rea analyze /absolute/path/to/program --provider ghidra --snapshot /absolute/path/to/analysis/program-6.2.json
```
In MCP, open the target without the incompatible `snapshot_path`, run the
required queries, then save to a new path through `close_binary.snapshot_path`.
Do not fabricate missing provider profiles or query bindings. See
[snapshot usage](cli.md#save-and-reuse-analysis-snapshots).
Artifact inventory nodes identify content bytes. Path-specific role and
permissions now belong to each occurrence:
| Previously read | Read in 6.2 |
| -------------------------------- | ----------------------------------------- |
| `nodes[].kind` | `occurrences[].artifact_kind` |
| Path-specific `nodes[].format` | `occurrences[].artifact_format` |
| `nodes[].executable` | `occurrences[].executable` |
| Content identity and byte format | `nodes[].artifact_id`, `sha256`, `format` |
Join an occurrence to its content node using `artifact_id`; inspect the selected
occurrence's role rather than choosing one role for all paths with equal bytes.
An executable binary format can have `artifact_kind: "executable"` while its
current filesystem permission produces `executable: false`. A `.js` and `.txt`
file with identical bytes can share a node and retain different roles.
Regenerate old inventories: a merged content node alone cannot recover the
per-path facts that were lost. These changes are part of
[63bf914](https://github.com/morluto/rea/commit/63bf91405cf97b114a84f337fb1272d98a267999).
## Process captures and cleanup failures
REA no longer converts legacy v3 process captures on import. Captures require
explicit selected/launch executable identity and `event_journal`; missing data
is rejected instead of being filled during parsing. Preserve old captures for
historical reference and recapture with 6.2 when current comparisons are needed.
Do not infer the original launched binary from a later file digest, or invent
event ordering from independently recorded collections. An explicitly empty
journal is valid under its advertised contract; the field cannot be omitted.
See [2a77469](https://github.com/morluto/rea/commit/2a7746998d47446c4db6d4f524e0f183d6803f3f).
Filesystem effects can now report `status: "unknown"` with a reason when a
partial snapshot cannot establish creation or deletion. Update exhaustive
status readers and retain observed facts separately from absence assumptions.
A completed one-shot analysis can report `cleanup_incomplete` rather than
silently returning success when provider cleanup fails. Preserve its
`details.partial_observation` or primary execution diagnostics, inspect the
reported owned resources, and address cleanup before retrying. A successful
analysis observation does not prove successful cleanup. This behavior is covered
by [#1211](https://github.com/morluto/rea/pull/1211). That change also makes
configured agent-evaluation fixture checks grade cited Evidence; routing or
answer wording alone no longer passes an assessed fixture.
## JavaScript and function comparisons
JavaScript return-field projections require `presence` with `present`, `absent`
or `unknown-coverage`. Export-shape change rows retain both sides' presence,
and `property_inventories` report examined properties and coverage. Unknown
values and unknown presence are distinct; callers must not treat an unknown
value as a missing property. Semantic object-operation graph observations use
`properties.property_path`, an array of complete path segments or `null`,
rather than a flattened property name. Regenerate old application analysis
before depending on nested mutation or presence facts. See
[475a0f8](https://github.com/morluto/rea/commit/475a0f84dac63d710e032bc7f731c1a3bda624ec).
Function comparison dimension statuses are `unchanged`, `changed` or `unknown`.
The unreachable `truncated` status and `summary.truncated` counter have been
removed. Update exhaustive enum handling and display logic; unavailable facts
remain `unknown`. See
[5510b27](https://github.com/morluto/rea/commit/5510b27d7f863a3df26212fba4a59f8df0da3712).
## Historical-source symlinks
Source graph symlinks with `target_state: "unreadable"` or `"unknown"` require
`target: null`. Internal, external and missing targets retain their actual
recorded target strings. External targets use the observed absolute target;
display placeholders such as `<outside-root>` cannot serve as lookup data.
Reimport the source tree when the old graph retained only a placeholder, and
handle nullable targets in custom readers. See
[b7b4333](https://github.com/morluto/rea/commit/b7b433370310c3ffe33d5027d8393151d5e862da).
Keep original saved artifacts until their replacements and consuming workflows
have been checked. Fresh analysis may establish different facts if the target,
source tree or runtime changed; it does not rewrite what an older capture proved.
+35 -3
View File
@@ -112,14 +112,17 @@ 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. It preserves ordinary counters, dimensions, file
line numbers, and ambiguous bare host labels. Use explicit literal `patterns`
ports from 0 through 65535. A port may end a sentence, as in `port: 8080.`;
a period followed by a letter or digit continues a decimal, version, or host
name instead. Each PTY chunk is normalized on its own, so a port whose period
ends the chunk is left unchanged because its continuation is not yet known. It preserves ordinary counters, dimensions, file line numbers,
and ambiguous bare host labels. Use explicit literal `patterns`
for a different application-specific spelling. Set `ports: false` to preserve
all endpoint numbers in comparison text. Captures commit the port-normalization
version so older broad numeric normalization cannot silently compare as the
same contract.
Output, file count, file size, process sampling, filesystem depth, total
Output retention, file count, aggregate file-hashing bytes, process sampling, filesystem depth, total
runtime, idle time, and post-exit settlement are bounded by the scenario's
limits. The result marks truncated observations and residual unknowns rather
than treating missing data as proof of equivalence. Cancellation and timeout
@@ -127,6 +130,35 @@ run the same owned-process cleanup path. Settlement reports whether the
sampled process group quiesced or whether cleanup was needed or unverifiable;
sampling cannot prove that every short-lived or detached descendant was seen.
New captures include `truncation_details`, with separate accounting for:
- `raw_terminal`: original UTF-8 PTY chunk bytes and observed/retained chunk
counts. A chunk that does not fit `limits.output_bytes` is omitted whole;
a later smaller chunk can still fit.
- `rendered_terminal`: cumulative UTF-8 serialized-state and visible-line
bytes, plus observed/retained frame counts. This is independent of the raw
chunk budget and does not count JSON encoding. Repeated screen snapshots can
exhaust it even when all raw output fits. Rendered states use retained PTY
input, so raw omissions also limit rendered coverage.
- `filesystem_before` and `filesystem_after`: file-count/depth limits,
enumeration failures, whole-file hash budget and bytes successfully hashed.
Each retained regular file without a digest has an aliased path, size,
remaining budget and reason: `file_bytes_budget` or
`file_changed_or_short_read`. A file too large for the remaining budget is
skipped; a later smaller file can still be hashed. Hash omissions do not
imply incomplete path enumeration.
- `process`: sampling limit and whether sampling ended partially. Coverage
remains `sampled`, including when that limit was not exhausted.
The legacy `truncated` flag summarizes these observations. Comparisons retain
results for unaffected dimensions and mark affected dimensions unknown; an
incomplete capture cannot locate the first divergence across all dimensions.
Trace assertions use coverage for the sources they select, so an assertion
about complete raw terminal output need not fail because rendered snapshots
were omitted. Older truncated captures without these details retain the
conservative comparison behavior. These diagnostics use the existing scenario
budgets; they do not introduce a separate file-hashing budget.
When the host withholds an unrelated process’s ownership token, REA leaves that
process untouched and records its PID and reason in `cleanup.unverified_processes`
and process residual unknowns. Successful cleanup verifies the owned group;
+3
View File
@@ -35,6 +35,9 @@ registry metadata or changelog disagrees with that version before creating a tag
### Compatibility and version selection
For the 6.2 candidate, review the [6.1 to 6.2 migration guide](migration-6.2.md)
alongside the changelog before publication.
REA uses Release Please's `always-bump-minor` strategy: every release increments
minor and resets patch, including releases with breaking changes. For example,
the next release after 6.0.0 is 6.1.0. Version numbers use the SemVer format,
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "rea-agents",
"version": "6.1.0",
"version": "6.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rea-agents",
"version": "6.1.0",
"version": "6.2.0",
"license": "MIT",
"dependencies": {
"@babel/parser": "8.0.4",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "rea-agents",
"mcpName": "io.github.morluto/rea",
"version": "6.1.0",
"version": "6.2.0",
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
"license": "MIT",
"repository": {
@@ -232,7 +232,7 @@
"oxfmt",
"oxlint --fix"
],
"*.{json,md,yml,yaml}": "oxfmt"
"*.{json,md,yml,yaml}": "oxfmt --no-error-on-unmatched-pattern"
},
"dependencies": {
"@babel/parser": "8.0.4",
@@ -7,6 +7,10 @@ export async function startBrowserVerifierSite() {
let port = 0;
let sessionGeneration = 0;
const server = createServer((request, response) => {
if (request.url === "/failed-navigation") {
response.destroy();
return;
}
if (request.url === "/slow-json") {
response.setHeader("content-type", "application/json");
response.write('{"pending":');
+72
View File
@@ -1,9 +1,14 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { readdir } from "node:fs/promises";
import { tmpdir } from "node:os";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { browserScenarioSchema } from "../../dist/domain/browserScenario.js";
import { requireMcpToolError } from "./mcp-verifier-results.mjs";
const execute = promisify(execFile);
@@ -142,6 +147,73 @@ export async function runScenarioCli(scenario) {
return JSON.parse(stdout);
}
/** Verify failed real navigation retains observations through both public adapters. */
export async function verifyScenarioFailureEvidence(executable, origin) {
const scenario = browserScenarioSchema.parse({
browser: { mode: "launch", executable_path: executable },
start_url: { url: `${origin}/failed-navigation` },
actions: [
{ step_id: "unused", action: "wait_for_timeout", duration_ms: 1 },
],
capture: { events: ["network"] },
});
const profilesBefore = await scenarioProfiles();
let cliFailure;
try {
await runScenarioCli(scenario);
assert.fail("The fixture must fail before initial navigation completes");
} catch (cause) {
assert.equal(typeof cause.stdout, "string");
cliFailure = JSON.parse(cause.stdout);
}
assertFailedNavigation(cliFailure, scenario.start_url.url);
const transport = new StdioClientTransport({
command: process.execPath,
args: [fileURLToPath(new URL("../rea.mjs", import.meta.url)), "mcp"],
env: process.env,
stderr: "pipe",
});
const client = new Client({
name: "browser-scenario-failure-e2e",
version: "1",
});
try {
await client.connect(transport);
const result = await client.callTool({
name: "capture_browser_scenario",
arguments: scenario,
});
assert.equal(result.isError, true);
assertFailedNavigation(requireMcpToolError(result), scenario.start_url.url);
} finally {
await client.close();
await transport.close();
}
const profilesAfter = await scenarioProfiles();
assert.ok([...profilesAfter].every((profile) => profilesBefore.has(profile)));
return {
mocked: false,
cli: true,
stdio_mcp: true,
failed_navigation_events_retained: true,
profile_cleanup: true,
};
}
const assertFailedNavigation = (error, requestedUrl) => {
assert.equal(error?.code, "execution_failure");
const observation = error.details?.partial_observation;
assert.equal(observation?.kind, "browser-scenario-observation");
assert.equal(observation.capture.browser.cleanup, "terminated-owned-process");
assert.deepEqual(observation.capture.steps, []);
assert.ok(
observation.capture.events.items.some(
(event) =>
event.kind === "request-failed" && event.url.url === requestedUrl,
),
);
};
/** Snapshot provider-owned profile names for cleanup comparison. */
export async function scenarioProfiles() {
return new Set(
+3 -2
View File
@@ -1,3 +1,4 @@
import { requireMcpToolError } from "./mcp-verifier-results.mjs";
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { mkdtemp, readFile, rm } from "node:fs/promises";
@@ -158,11 +159,11 @@ const verifyDefaultScreenshotDelivery = async ({
);
assert.equal(constrained.isError, true);
assert.equal(
constrained.structuredContent?.error?.details?.resource,
requireMcpToolError(constrained)?.details?.resource,
"transport",
);
const reference =
constrained.structuredContent?.error?.details?.reported_limits
requireMcpToolError(constrained)?.details?.reported_limits
?.evidence_reference;
assert.equal(reference?.kind, "retained-evidence");
await client.ping();
+2 -1
View File
@@ -35,6 +35,7 @@ export const assertSameNames = (label, actual, expected) => {
const SOURCE_PATHS = {
packageMetadata: "dist/generatedPackageMetadata.js",
catalogIdentity: "dist/catalogIdentity.js",
cliCommandNames: "dist/cliCommandNames.js",
cli: "dist/cli.js",
toolContracts: "dist/contracts/toolContracts.js",
officialContracts: "dist/contracts/officialToolContracts.js",
@@ -76,7 +77,7 @@ const SOURCE_PATHS = {
nativeProvider: "dist/native/NativeMacOSProviderMetadata.js",
artifactProviders: "dist/application/InvestigationProviders.js",
browserIdentities: "dist/browser/providerIdentities.js",
v8InspectorProvider: "dist/inspector/V8InspectorProvider.js",
v8InspectorProvider: "dist/inspector/providerIdentity.js",
evidence: "dist/domain/evidence.js",
evidenceBundle: "dist/domain/evidenceBundle.js",
evidenceCompletion: "dist/domain/evidenceCompletionLedger.js",
@@ -1,3 +1,8 @@
import {
requireMcpToolError,
requireMcpLifecycleResult,
requireMcpEvidenceResult,
} from "./mcp-verifier-results.mjs";
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { randomUUID } from "node:crypto";
@@ -15,10 +20,6 @@ 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(
@@ -124,7 +125,7 @@ export async function verifyLegacyGhidraReferenceSnapshot(
},
});
assert.equal(reply.isError, true);
const mcpRejection = reply.structuredContent.error;
const mcpRejection = requireMcpToolError(reply);
assert.equal(mcpRejection.code, "evidence_integrity_mismatch");
assert.match(mcpRejection.details.reason, /profile_mismatch/u);
assert.match(
+7
View File
@@ -1,3 +1,4 @@
import { analysisErrorProjectionSchema } from "../../dist/contracts/errorSchemas.js";
import { toolContract } from "../../dist/contracts/toolContracts.js";
/** Read the textual JSON projection from one MCP tool call. */
@@ -9,6 +10,12 @@ 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,6 +25,15 @@ const environment = () => ({
...(process.env.JAVA_HOME === undefined
? {}
: { JAVA_HOME: process.env.JAVA_HOME }),
...(process.env.GHIDRA_HEADLESS_MAXMEM === undefined
? {}
: { GHIDRA_HEADLESS_MAXMEM: process.env.GHIDRA_HEADLESS_MAXMEM }),
...(process.env.GHIDRA_MAXMEM === undefined
? {}
: { GHIDRA_MAXMEM: process.env.GHIDRA_MAXMEM }),
...(process.env._JAVA_OPTIONS === undefined
? {}
: { _JAVA_OPTIONS: process.env._JAVA_OPTIONS }),
});
const verifyGraph = (input, target, procedure, globalAddress) => {
+1 -1
View File
@@ -24,7 +24,7 @@ export const createProductCatalog = async (root) => {
assertSameNames(
"Primary CLI inventory",
cli.primary,
sources.catalogIdentity.CLI_COMMAND_NAMES,
sources.cliCommandNames.CLI_COMMAND_NAMES,
);
const tools = toolFamilyCatalog(sources);
const providers = providerCatalog(sources);
@@ -1,3 +1,4 @@
import { requireMcpToolError } from "./mcp-verifier-results.mjs";
import assert from "node:assert/strict";
/** Exercise segmented-address truncation through real CLI/MCP reads and edits. */
@@ -40,13 +41,14 @@ export async function verifyGhidraAddressBoundaries(client, entry, cli) {
{ timeout: 240000 },
);
assert.equal(reply.isError, true, `${name} accepted ${address}`);
const error = reply.structuredContent?.error;
const error = requireMcpToolError(reply);
assert.equal(error?.code, "invalid_request", JSON.stringify(reply));
assert.equal(error.details.operation, name);
const issues = JSON.stringify(error.details.issues);
assert.match(issues, /cannot be represented without truncation/u);
assert.ok(issues.includes(address), issues);
assert.equal(reply.structuredContent.evidence, undefined);
// A rejected request produces no Evidence; errors carry text only.
assert.equal(reply.structuredContent, undefined);
}
}
assert.deepEqual(
+16 -17
View File
@@ -1,3 +1,8 @@
import {
requireMcpToolError,
mcpTextValue,
requireMcpOperationResult,
} from "./mcp-verifier-results.mjs";
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
@@ -7,11 +12,9 @@ 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";
@@ -61,23 +64,19 @@ export async function verifyGhidraBoundaries(
true,
`${name} accepted ${JSON.stringify(args)}`,
);
// SDK input-schema rejections are text-only; application errors use the
// canonical structured error projection, separately from success schemas.
if (reply.structuredContent !== undefined) {
analysisErrorProjectionSchema.parse(reply.structuredContent.error);
assert.deepEqual(
reply.structuredContent,
JSON.parse(mcpTextValue(reply)),
);
}
// SDK input rejections are prose; application errors carry canonical JSON
// text without publishing data outside the advertised success schema.
const error = mcpTextValue(reply).startsWith("{")
? requireMcpToolError(reply)
: undefined;
assert.equal(reply.structuredContent, undefined);
if (diagnostic !== undefined) {
const error = reply.structuredContent?.error;
assert.equal(error?.code, "invalid_request");
assert.equal(error?.details?.operation, name);
assert.match(JSON.stringify(error.details.issues), diagnostic);
}
rejectedCalls++;
return reply.structuredContent?.error;
return error;
};
const cli = async (command, value, flags = []) => {
const { stdout } = await promisify(execFile)(
@@ -481,7 +480,7 @@ export async function verifyGhidraBoundaries(
options,
);
assert.equal(regexFailure.isError, true);
const regexError = regexFailure.structuredContent.error;
const regexError = requireMcpToolError(regexFailure);
assert.equal(regexError.code, "resource_constraint");
assert.equal(regexError.details.operation, "search_strings");
assert.equal(regexError.details.resource, "memory");
@@ -503,7 +502,7 @@ export async function verifyGhidraBoundaries(
options,
);
assert.equal(compileFailure.isError, true);
const compileError = compileFailure.structuredContent.error;
const compileError = requireMcpToolError(compileFailure);
assert.equal(compileError.code, "resource_constraint");
assert.equal(compileError.details.operation, "search_procedures");
assert.match(compileError.details.reason, /stack while compiling pattern/u);
+9 -4
View File
@@ -64,11 +64,13 @@ export async function verifyGhidraLargeResults({
assert.equal(expected[0], 0x5a);
assert.equal(expected.at(-1), 0xa5);
const before = await call("binary_session");
// This request fits the SDK receive limit, but repeating the producer's
// diagnostic in text and structured content cannot fit the response.
// An error reaches the wire once, as escaped JSON text. Each quote costs
// two request bytes but four response bytes once the echoed diagnostic is
// escaped again as text, so this request fits the SDK receive limit while
// the error response cannot.
const missingSelector =
"REA_MISSING_" +
"x".repeat(Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 2) + 64 * 1024);
'"'.repeat(Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 4) + 64 * 1024);
const lookupError = await reject("procedure_address", {
procedure: missingSelector,
});
@@ -87,9 +89,12 @@ export async function verifyGhidraLargeResults({
length,
});
const readId = retainedReference(readError, "read_bytes");
// Two annotation strings repeated in text and structured content must
// exceed the receive budget even without obsolete Evidence envelopes.
// The request contains only the two strings and remains below that budget.
const comment =
"REA_BIG_COMMENT:" +
"x".repeat(Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 10));
"x".repeat(Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 4));
const annotationError = await reject("annotate_native_function", {
procedure: entry.address,
name: "rea_frame_probe",
@@ -1,10 +1,13 @@
import {
requireMcpToolError,
requireMcpOperationResult,
} from "./mcp-verifier-results.mjs";
import assert from "node:assert/strict";
import { access, mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { parseAnalysisSnapshot } from "../../dist/domain/analysisSnapshot.js";
import { parseEvidenceBundle } from "../../dist/domain/evidenceBundle.js";
import { requireMcpOperationResult } from "./mcp-verifier-results.mjs";
/** Keep mutable Ghidra observations out of immutable snapshots across target opens. */
export async function verifyGhidraSnapshotLifecycle(
@@ -31,7 +34,7 @@ export async function verifyGhidraSnapshotLifecycle(
true,
`${name} accepted a mutated snapshot binding`,
);
const error = reply.structuredContent?.error;
const error = requireMcpToolError(reply);
assert.equal(
error?.code,
"evidence_integrity_mismatch",
+1 -1
View File
@@ -1,5 +1,5 @@
import assert from "node:assert/strict";
import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProvider.js";
import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProviderCapabilities.js";
import {
requireMcpEvidenceResult,
requireEvidenceProvider,
+1 -1
View File
@@ -1,4 +1,4 @@
import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProvider.js";
import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProviderCapabilities.js";
/** Verify the selected Hopper candidate and concrete provider binding. */
export const requireHopperSelection = (status, expected) => {
+75 -2
View File
@@ -1,10 +1,16 @@
"""Package the public Notes example for the static website."""
"""Prepare downloads, the sitemap and the sharing image for the static website."""
from html.parser import HTMLParser
from io import BytesIO
from pathlib import Path
import re
import sys
from urllib.parse import quote
import xml.etree.ElementTree as ET
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
SITE_ORIGIN = "https://rea.tools"
EXAMPLE_FILES = (
"package.json",
"main.js",
@@ -15,6 +21,70 @@ EXAMPLE_FILES = (
)
class IndexPolicy(HTMLParser):
"""Read indexing directives from the authored HTML head."""
def __init__(self):
super().__init__()
self.in_head = False
self.noindex = False
def handle_starttag(self, tag, attrs):
if tag == "head":
self.in_head = True
if tag == "meta" and self.in_head:
attributes = dict(attrs)
if (attributes.get("name") or "").lower() in ("robots", "googlebot"):
directives = re.split(
r"[\s,]+", (attributes.get("content") or "").lower()
)
self.noindex |= bool(set(directives) & {"noindex", "none"})
def handle_endtag(self, tag):
if tag == "head":
self.in_head = False
def prepare_sitemap(site):
"""Discover HTML routes automatically, excluding pages marked noindex."""
namespace = "http://www.sitemaps.org/schemas/sitemap/0.9"
ET.register_namespace("", namespace)
sitemap = ET.Element(f"{{{namespace}}}urlset")
for path in sorted(site.rglob("*.html")):
policy = IndexPolicy()
policy.feed(path.read_text(encoding="utf-8"))
if policy.noindex:
continue
route = path.relative_to(site).as_posix()
if path.name == "index.html":
route = route.removesuffix("index.html")
entry = ET.SubElement(sitemap, f"{{{namespace}}}url")
ET.SubElement(
entry, f"{{{namespace}}}loc"
).text = f"{SITE_ORIGIN}/{quote(route, safe='/')}"
ET.indent(sitemap, space=" ")
ET.ElementTree(sitemap).write(
site / "sitemap.xml", encoding="utf-8", xml_declaration=True
)
print(f"Prepared sitemap.xml ({len(sitemap)} indexable pages).")
def prepare_sharing_image(site):
"""Rasterize the maintained SVG for clients that need a PNG preview."""
try:
import cairosvg
except (ImportError, OSError) as error:
sys.exit(
f"Cannot prepare sharing image: {error}. Install website/requirements.txt and native Cairo; see website/README.md."
)
source = site / "assets/social-card.svg"
destination = site / "assets/social-card.png"
cairosvg.svg2png(
url=str(source), write_to=str(destination), output_width=1200, output_height=630
)
print("Prepared assets/social-card.png (1200 × 630).")
def prepare_notes_example(site):
"""Create a ZIP with public source only and reproducible metadata."""
source = site / "examples/notes-electron"
@@ -37,4 +107,7 @@ def prepare_notes_example(site):
if __name__ == "__main__":
root = Path(__file__).resolve().parent.parent
prepare_notes_example(root / "website/public")
site = root / "website/public"
prepare_sharing_image(site)
prepare_sitemap(site)
prepare_notes_example(site)
+130
View File
@@ -0,0 +1,130 @@
"""Exercise automatic route discovery and mistakes that break search metadata."""
import contextlib
import importlib.util
import io
from pathlib import Path
import shutil
import tempfile
import unittest
import xml.etree.ElementTree as ET
ROOT = Path(__file__).resolve().parent.parent
def load_script(name):
"""Load a repository script without executing its command-line entrypoint."""
spec = importlib.util.spec_from_file_location(name, ROOT / "scripts" / f"{name}.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
PREPARE = load_script("prepare-website")
VERIFY = load_script("verify-website")
class WebsiteSearchChecks(unittest.TestCase):
"""Use authored pages and temporary fixtures to check the publishing boundary."""
def test_sitemap_tracks_added_moved_removed_and_nonindexable_pages(self):
with tempfile.TemporaryDirectory() as directory:
site = Path(directory)
def page(name, head="", body=""):
path = site / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(f"<html><head>{head}</head><body>{body}</body></html>")
return path
page("index.html")
nested = page("guides/new/index.html")
page(
"examples/fixture/index.html",
'<meta name="robots" content="NOINDEX, follow">',
)
page("draft.html", '<meta name="googlebot" content="none">')
page("code & notes.html")
page("visible/index.html", body='<meta name="robots" content="noindex">')
(site / "asset.json").write_text("{}")
def urls():
with contextlib.redirect_stdout(io.StringIO()):
PREPARE.prepare_sitemap(site)
sitemap = ET.parse(site / "sitemap.xml")
return {node.text for node in sitemap.findall(".//{*}loc")}
self.assertEqual(
urls(),
{
"https://rea.tools/",
"https://rea.tools/guides/new/",
"https://rea.tools/code%20%26%20notes.html",
"https://rea.tools/visible/",
},
)
nested.rename(site / "moved.html")
self.assertIn("https://rea.tools/moved.html", urls())
self.assertNotIn("https://rea.tools/guides/new/", urls())
(site / "moved.html").unlink()
self.assertNotIn("https://rea.tools/moved.html", urls())
def test_verifier_rejects_wrong_canonicals_stale_sitemaps_and_missing_previews(
self,
):
cases = (
(
"index.html",
'rel="canonical" href="https://rea.tools/"',
'rel="canonical" href="https://morluto.github.io/rea/"',
"head canonical",
),
(
"showcase/aegis/index.html",
'rel="canonical" href="https://rea.tools/showcase/aegis/"',
'rel="canonical" href="https://rea.tools/"',
"head canonical",
),
(
"examples/notes-electron/index.html",
'content="noindex"',
'content="index"',
"must have a head noindex",
),
(
"sitemap.xml",
"https://rea.tools/faq/",
"https://rea.tools/deleted/",
"each indexable page once",
),
(
"faq/index.html",
'name="twitter:card" content="summary_large_image"',
'name="twitter:card" content="summary"',
"twitter:card",
),
)
for name, old, new, message in cases:
with self.subTest(name=name), tempfile.TemporaryDirectory() as directory:
site = Path(directory) / "public"
shutil.copytree(ROOT / "website/public", site)
path = site / name
source = path.read_text()
self.assertIn(old, source)
path.write_text(source.replace(old, new, 1))
self.assertTrue(
any(message in error for error in VERIFY.check_site(site)[3])
)
with tempfile.TemporaryDirectory() as directory:
site = Path(directory) / "public"
shutil.copytree(ROOT / "website/public", site)
(site / "assets/social-card.png").unlink()
self.assertIn(
"Social preview PNG is missing; run scripts/prepare-website.py",
VERIFY.check_site(site)[3],
)
if __name__ == "__main__":
unittest.main()
+4 -3
View File
@@ -1,3 +1,4 @@
import { requireMcpToolError } from "./lib/mcp-verifier-results.mjs";
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
@@ -232,12 +233,12 @@ const checkHardenedRuntime = async (directory, binary) => {
{ timeout: CLI_TIMEOUT_MS },
);
assert.equal(deniedMcp.isError, true, JSON.stringify(deniedMcp));
assert.equal(deniedMcp.structuredContent.error.code, denied.code);
assert.equal(requireMcpToolError(deniedMcp).code, denied.code);
assert.match(
deniedMcp.structuredContent.error.details.reason,
requireMcpToolError(deniedMcp).details.reason,
/^debugger-attach-denied: /u,
);
assert.ok(deniedMcp.structuredContent.error.details.partial_observation);
assert.ok(requireMcpToolError(deniedMcp).details.partial_observation);
});
const allowed = join(directory, "debuggable");
const entitlements = join(directory, "get-task-allow.plist");
+2 -1
View File
@@ -1,3 +1,4 @@
import { requireMcpToolError } from "./lib/mcp-verifier-results.mjs";
import assert from "node:assert/strict";
import { join } from "node:path";
import { execFile } from "node:child_process";
@@ -50,7 +51,7 @@ const verifyMcpUnknownProvider = async (client, mcpOptions) => {
);
if (
unknownProvider.isError !== true ||
unknownProvider.structuredContent?.error?.details?.selection_reason !==
requireMcpToolError(unknownProvider)?.details?.selection_reason !==
"unknown_provider"
)
throw new Error("packaged MCP accepted an unknown analysis provider");
+7 -1
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/webCaptureDiff.js";
import { compareWebCapturesInputSchema } from "../dist/domain/webCaptureDiffSchemas.js";
import {
captureWebScreenshotInputSchema,
compareWebScreenshotsInputSchema,
@@ -33,6 +33,7 @@ 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";
@@ -294,6 +295,10 @@ 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,
@@ -360,6 +365,7 @@ 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/GhidraInstallation.js";
import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraInstallationPolicy.js";
import { GHIDRA_SESSION_CAPABILITIES } from "../dist/ghidra/GhidraSessionValues.js";
export function assertLocalProcedureInfo(info, expectedAddress) {
+3 -5
View File
@@ -16,12 +16,10 @@ import {
parseGhidraInventoryInput,
parseGhidraInventoryResult,
} from "../dist/ghidra/GhidraInventoryValues.js";
import {
inspectGhidraInstallation,
SUPPORTED_GHIDRA_VERSION,
} from "../dist/ghidra/GhidraInstallation.js";
import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js";
import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraInstallationPolicy.js";
import { GhidraHeadlessLauncher } from "../dist/ghidra/GhidraLauncher.js";
import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProvider.js";
import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProviderCapabilities.js";
import { windowsP0Capabilities } from "../dist/ghidra/GhidraProviderCapabilities.js";
import { GHIDRA_SESSION_CAPABILITIES } from "../dist/ghidra/GhidraSessionValues.js";
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
+3 -5
View File
@@ -10,12 +10,10 @@ 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,
SUPPORTED_GHIDRA_VERSION,
} from "../dist/ghidra/GhidraInstallation.js";
import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js";
import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraInstallationPolicy.js";
import { GhidraHeadlessLauncher } from "../dist/ghidra/GhidraLauncher.js";
import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProvider.js";
import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProviderCapabilities.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/HopperProvider.js";
import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProviderCapabilities.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.js";
import { parseConfig } from "../dist/config/parseConfig.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/HopperProvider.js";
import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProviderCapabilities.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";
+2 -1
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env node
import { requireMcpToolError } from "./lib/mcp-verifier-results.mjs";
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
@@ -172,7 +173,7 @@ const mcp = async (cliDossier) => {
arguments: { address: "not-an-address" },
});
assert.equal(invalid.isError, true);
assert.equal(invalid.structuredContent.error.code, "invalid_request");
assert.equal(requireMcpToolError(invalid).code, "invalid_request");
await call("close_binary", {});
opened = false;
report.checks.push(
+219 -10
View File
@@ -1,14 +1,19 @@
"""Check local website references and the sole GitHub Pages publisher."""
"""Check website references, search metadata and the sole production publisher."""
from html.parser import HTMLParser
from pathlib import Path
import re
import struct
import sys
from urllib.parse import unquote, urlsplit
from urllib.parse import quote, 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."""
@@ -17,9 +22,42 @@ 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:
@@ -30,6 +68,154 @@ 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."""
@@ -63,7 +249,11 @@ 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")):
@@ -71,18 +261,23 @@ def check_site(site):
ET.parse(path)
except ET.ParseError as error:
errors.append(f"{path.relative_to(site)}: invalid SVG XML: {error}")
return len(pages), references, errors
indexable, metadata_errors = check_search_metadata(site, pages)
errors.extend(metadata_errors)
return len(pages), references, indexable, 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"
@@ -95,7 +290,14 @@ 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():
@@ -103,7 +305,9 @@ 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()
@@ -116,13 +320,18 @@ 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, errors = check_site(root / "website/public")
pages, references, indexable, 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 {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("website-pages.yml is the sole GitHub Pages publisher.")
return 0
+8 -8
View File
@@ -1,3 +1,4 @@
import { requireMcpToolError } from "../../lib/mcp-verifier-results.mjs";
import assert from "node:assert/strict";
import {
access,
@@ -208,13 +209,12 @@ export async function verifyAndroidFailureRecovery({
{ timeout: 150_000 },
);
assert.equal(response.isError, true);
assert.equal(response.structuredContent.error.code, "invalid_request");
assert.deepEqual(
response.structuredContent.error.details.issues[0].path,
["class_name"],
);
assert.equal(requireMcpToolError(response).code, "invalid_request");
assert.deepEqual(requireMcpToolError(response).details.issues[0].path, [
"class_name",
]);
assert.match(
response.structuredContent.error.details.issues[0].message,
requireMcpToolError(response).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(method.structuredContent.error.code, "invalid_request");
assert.deepEqual(method.structuredContent.error.details.issues[0].path, [
assert.equal(requireMcpToolError(method).code, "invalid_request");
assert.deepEqual(requireMcpToolError(method).details.issues[0].path, [
"method_name",
]);
await client.ping();
@@ -0,0 +1,86 @@
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);
}
}
+4 -1
View File
@@ -16,6 +16,7 @@ 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);
@@ -67,7 +68,8 @@ const EXPECTED_BUNDLES = [
let report;
try {
const { app } = await buildMacosBundleFixture(root);
const { app, executable } = await buildMacosBundleFixture(root);
const machoOccurrences = await verifyMachOOccurrences(app, executable);
const zip = join(root, "MacFixture.zip");
await exec("/usr/bin/ditto", ["-c", "-k", "--keepParent", app, zip]);
const staging = join(root, "dmg-source");
@@ -164,6 +166,7 @@ try {
containers: containers.map(({ format }) => format),
bundles: EXPECTED_BUNDLES.length,
dmg_detached: true,
macho_occurrences: machoOccurrences,
dylib_resolution: dylibs,
dyld_environment: dyldEnvironment,
};
+3 -2
View File
@@ -1,3 +1,4 @@
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";
@@ -172,11 +173,11 @@ window.loadFile("renderer.html");require("node:fs").writeFileSync(${JSON.stringi
{ timeout: 150_000 },
);
const capture = active.isError
? active.structuredContent.error.details.partial_observation.capture
? requireMcpToolError(active).details.partial_observation.capture
: active.structuredContent.normalized_result;
if (active.isError) {
assert.equal(
active.structuredContent.error.code,
requireMcpToolError(active).code,
"cleanup_incomplete",
JSON.stringify(active),
);
+2 -2
View File
@@ -8,13 +8,13 @@
"url": "https://github.com/morluto/rea",
"source": "github"
},
"version": "6.1.0",
"version": "6.2.0",
"packages": [
{
"registryType": "npm",
"registryBaseUrl": "https://registry.npmjs.org",
"identifier": "rea-agents",
"version": "6.1.0",
"version": "6.2.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/binaryTarget.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.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/binaryTarget.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.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/binaryTarget.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import type { OfficialToolName } from "../contracts/officialToolContracts.js";
import type { EnhancedToolName } from "../contracts/enhancedInputs.js";
import type { NativeToolName } from "../contracts/native/nativeToolContracts.js";
+4 -2
View File
@@ -7,11 +7,13 @@ import {
import { jsonValueSchema } from "../domain/jsonValue.js";
import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
import type {
BrowserTargetList,
InspectWebPageInput,
ListBrowserTargetsInput,
WebPageInspection,
} from "../domain/browserObservation.js";
import type {
BrowserTargetList,
WebPageInspection,
} from "../domain/browserObservationSchemas.js";
import type {
AnalyzeWebBundleInput,
WebBundleAnalysis,
+4 -2
View File
@@ -1,10 +1,12 @@
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type {
BrowserTargetList,
InspectWebPageInput,
ListBrowserTargetsInput,
WebPageInspection,
} from "../domain/browserObservation.js";
import type {
BrowserTargetList,
WebPageInspection,
} from "../domain/browserObservationSchemas.js";
import type { Result } from "../domain/result.js";
import type {
AnalyzeWebBundleInput,
+3 -2
View File
@@ -6,7 +6,8 @@ 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, type Logger } from "../logger.js";
import { silentLogger } from "../logger.js";
import type { Logger } from "pino";
import { createEvidence } from "../domain/evidence.js";
import type { Evidence } from "../domain/evidence.js";
import type { NativeToolName } from "../contracts/native/nativeToolContracts.js";
@@ -24,7 +25,7 @@ import {
writeAnalysisSnapshot,
} from "./binary/AnalysisSnapshotFiles.js";
import { parseBinaryTarget } from "./BinaryTargetResolver.js";
import type { BinaryTarget } from "../domain/binaryTarget.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import {
snapshotEvidenceForQuery,
snapshotMatchesTarget,
@@ -1,5 +1,6 @@
import type { AppConfig, parseConfig } from "../config.js";
import type { Logger } from "../logger.js";
import type { AppConfig } from "../config/types.js";
import type { parseConfig } from "../config/parseConfig.js";
import type { Logger } from "pino";
import type { BinarySession } from "./binary/BinarySession.js";
/** Configuration and one-shot session factories supplied by the production boundary. */
+2 -1
View File
@@ -1,7 +1,8 @@
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { jsonObjectSchema, type JsonValue } from "../domain/jsonValue.js";
import { createServerIdentity } from "../serverIdentity.js";
import { silentLogger, type Logger } from "../logger.js";
import { silentLogger } from "../logger.js";
import type { Logger } from "pino";
import type { DirectAnalysisDependencies } from "./DirectAnalysisDependencies.js";
const runSessionStatus = async (
-2
View File
@@ -42,8 +42,6 @@ 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/processCapture.js";
import { parseProcessCapture } from "../domain/process/processCaptureParsing.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/binaryTarget.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.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/processCapture.js";
import { parseProcessCapture } from "../domain/process/processCaptureParsing.js";
import {
digestProcessCommitment,
parseProcessScenario,
@@ -7,7 +7,7 @@ import {
parseEvidenceBundle,
type EvidenceBundle,
} from "../domain/evidenceBundle.js";
import { parseProcessCapture } from "../domain/process/processCapture.js";
import { parseProcessCapture } from "../domain/process/processCaptureParsing.js";
import type {
ReconstructionObligation,
ReviewedReconstructionObligation,
@@ -1,13 +1,12 @@
import { PROCESS_PROVIDER } from "./process/ProcessEvidence.js";
import { PROCESS_PROVIDER } from "../domain/process/processEvidenceProvider.js";
import {
buildReconstructionObligationLedgerEvidenceValidated,
resolveReconstructionObligationLedgerRequest,
} from "./ReconstructionObligationLedgerService.js";
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/process/processCapture.fixture.js";
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/process/processCaptureExample.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,
@@ -15,6 +14,7 @@ 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,11 +17,6 @@ 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/ReferenceSourceReader.js";
} from "../reference/ReferenceSourceReaderTypes.js";
import { PARSEABLE_REFERENCE_SOURCE_LANGUAGES } from "./ReferenceSourceImportTypes.js";
export interface ParsedReferenceSourceEntries {
-1
View File
@@ -40,7 +40,6 @@ 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/binaryTarget.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.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/binaryTarget.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.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/binaryTarget.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.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/binaryTarget.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.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/binaryTarget.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
@@ -1,4 +1,4 @@
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import type { EvidenceBundle } from "../../domain/evidenceBundle.js";
+1 -2
View File
@@ -1,6 +1,6 @@
import { randomUUID } from "node:crypto";
import type { BinaryTarget } from "../../domain/binaryTarget.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import {
analysisProfilesEqual,
type AnalysisProfileCommitment,
@@ -48,7 +48,6 @@ 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/binaryTarget.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.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/binaryTarget.js";
import { type BinaryTarget } from "../../domain/binaryTargetTypes.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/binaryTarget.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.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/binaryTarget.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.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/binaryTarget.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.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/binaryTarget.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.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/binaryTarget.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.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/binaryTarget.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.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/binaryTarget.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.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/binaryTarget.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.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/binaryTarget.js";
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import type {
RecordUnknownInput,
@@ -1,4 +1,4 @@
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.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/javascriptApplicationGraph.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.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/javascriptApplicationGraph.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
import type {
ElectronBrowserWindowFinding,
ElectronContextBridgeFinding,
@@ -1,9 +1,11 @@
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 "../../artifacts/inventory/ArtifactInventory.js";
import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.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/javascriptApplicationGraph.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.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 "../../artifacts/inventory/ArtifactInventory.js";
import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js";
import {
completeApplicationCoverage,
partialApplicationCoverage,
type ApplicationGraphEvidence,
} from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.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/javascriptApplicationGraph.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.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 "../../artifacts/inventory/ArtifactInventory.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
import { completeApplicationCoverage } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import type {
@@ -21,7 +21,6 @@ import {
staticInferenceEvidence,
} from "./JavaScriptArtifactGraphEvidence.js";
import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js";
export { addJavaScriptBundlerNodes } from "./JavaScriptArtifactGraphBundlers.js";
interface PackageRoleInput {
readonly packageNode: ApplicationNode;
@@ -5,7 +5,9 @@ import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptA
import {
admitsCanonicalPathSyntax,
hasScheme,
htmlUrlText,
looksExternal,
percentDecodeUrlPath,
stripQueryAndFragment,
} from "../../domain/artifactPathSyntax.js";
@@ -248,7 +250,7 @@ const hasContainerCandidate = (
const htmlCandidate = (
input: ResolveArtifactPathInput,
): string | ArtifactPathResolution => {
const declared = stripQueryAndFragment(input.declaredPath);
const declared = stripQueryAndFragment(htmlUrlText(input.declaredPath));
if (looksExternal(declared))
return unresolvedOutcome(input, "external", [
"External HTML references are not mapped to local artifact assets.",
@@ -257,14 +259,25 @@ const htmlCandidate = (
const base =
rawBase === undefined || rawBase === null
? rawBase
: stripQueryAndFragment(rawBase);
: stripQueryAndFragment(htmlUrlText(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.",
]);
if (declared.startsWith("/")) return declared.slice(1);
// 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 (base === undefined || base === null || base === "")
return posix.join(posix.dirname(input.sourcePath), declared);
return posix.join(posix.dirname(input.sourcePath), declaredPath);
// 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.
@@ -272,10 +285,15 @@ 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 basePath = base.startsWith("/")
? base.slice(1)
: posix.join(posix.dirname(input.sourcePath), base);
return posix.join(htmlBaseDirectory(base, basePath), declared);
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);
};
/**
@@ -18,7 +18,7 @@ import {
javascriptArtifactReconstructionInputSchema,
type JavaScriptArtifactReconstructionInput,
} from "./JavaScriptArtifactReconstructionInput.js";
import { scanCanonicalArtifactInventory } from "../../artifacts/inventory/ArtifactInventory.js";
import { scanCanonicalArtifactInventory } from "../../artifacts/inventory/scanCanonical.js";
import { summarizeElectronBoundaries } from "./ElectronBoundaryAnalysis.js";
import { createJavaScriptSemanticGraphProjection } from "./JavaScriptSemanticGraphBuilder.js";
import type { ProgressReporter } from "../ProgressReporter.js";
@@ -109,3 +109,73 @@ 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/javascriptApplicationGraph.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
import {
completeApplicationCoverage,
partialApplicationCoverage,
@@ -1,5 +1,5 @@
import { createJavaScriptSemanticGraphUnknown } from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import type {
JavaScriptSemanticEventOperation,
JavaScriptSemanticTimerOperation,
@@ -10,10 +10,7 @@ import {
addSemanticGraphUnknown,
} from "./JavaScriptSemanticGraphConstruction.js";
import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
import {
semanticCallSiteAt,
type SemanticFlowProjectionContext,
} from "./JavaScriptSemanticGraphFlowProjection.js";
import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js";
/** Project literal EventEmitter registrations, removals, and dispatches. */
export const projectSemanticEvents = (
@@ -175,7 +172,7 @@ const projectTimerSchedule = (
timerNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
): void => {
addSemanticGraphRelation(context.state, {
source: semanticCallSiteAt(context, operation.location),
source: context.callSiteAt(operation.location),
target: timerNodes.get(operation.timerId),
relation: "schedules-timer",
resolution: "resolved",
@@ -199,7 +196,7 @@ const projectTimerCancellation = (
operation: JavaScriptSemanticTimerOperation,
timerNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
): void => {
const callSite = semanticCallSiteAt(context, operation.location);
const callSite = context.callSiteAt(operation.location);
const target =
operation.linkedTimerId === null
? undefined
@@ -838,6 +838,85 @@ 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/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import type {
JavaScriptSemanticChildProcessInteraction,
JavaScriptSemanticChildProcessSpawn,
@@ -10,10 +10,7 @@ import {
addSemanticGraphUnknown,
} from "./JavaScriptSemanticGraphConstruction.js";
import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
import {
semanticCallSiteAt,
type SemanticFlowProjectionContext,
} from "./JavaScriptSemanticGraphFlowProjection.js";
import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js";
interface ChildUnknownInput {
readonly context: SemanticFlowProjectionContext;
@@ -75,7 +72,7 @@ const projectSpawn = (
spawn: JavaScriptSemanticChildProcessSpawn,
child: JavaScriptSemanticGraphNode,
): void => {
const callSite = semanticCallSiteAt(context, spawn.location);
const callSite = context.callSiteAt(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/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import type {
JavaScriptSemanticBoundaryOperation,
JavaScriptSemanticConfigurationOperation,
@@ -11,10 +11,7 @@ import {
addSemanticGraphUnknown,
} from "./JavaScriptSemanticGraphConstruction.js";
import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
import {
semanticCallSiteAt,
type SemanticFlowProjectionContext,
} from "./JavaScriptSemanticGraphFlowProjection.js";
import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js";
/** Project environment, argv, file, default, and precedence candidates. */
export const projectSemanticConfiguration = (
@@ -158,7 +155,7 @@ const projectRequestConstruction = (
operation: JavaScriptSemanticRequestOperation,
request: JavaScriptSemanticGraphNode,
): void => {
const callSite = semanticCallSiteAt(context, operation.location);
const callSite = context.callSiteAt(operation.location);
addSemanticGraphRelation(context.state, {
source: callSite,
target: request,
@@ -197,7 +194,7 @@ const projectResponseConsumer = (
): void => {
addSemanticGraphRelation(context.state, {
source: response,
target: semanticCallSiteAt(context, operation.location),
target: context.callSiteAt(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/javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.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,
location: JavaScriptSourceRange | null,
): ApplicationGraphEvidence => ({
...observedSemanticEvidence(file, location),
authority: "unknown",

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