Compare commits

..
Author SHA1 Message Date
Cursor Agent 217c84ea7b test(desktop): expect each exported task summary
The export result lists task summaries on tasks. The seed-policy check
was still looking for a single task object.
2026-11-10 11:39:02 +00:00
Cursor Agent e9d620b400 merge main into fix/8545-bulk-append-overflow 2026-11-10 10:52:31 +00:00
Cursor Agent fdee758682 fix: keep unrecorded alias kind omitted and endpoint cleanup attached
A capture that does not record selected-path kind must not gain an
unknown kind on the export plan. An oversized Windows Ghidra endpoint
whose close fails stays an invalid endpoint and keeps that cleanup.
2026-11-10 10:37:28 +00:00
Cursor Agent 7b5ea25e4c merge main into fix/8545-bulk-append-overflow
Keep appendAll at the merged Bun and capture call sites, and use the
recoverable reader limits from main.
2026-11-10 10:18:19 +00:00
Cursor Agent d2f1212c50 merge main into fix/8545-bulk-append-overflow 2026-11-10 06:59:27 +00:00
Cursor Agent e0044a1a51 merge main into fix/8545-bulk-append-overflow 2026-11-10 05:52:28 +00:00
Cursor Agent 37ece4a8ba merge main into fix/8545-bulk-append-overflow
Keep main's newer observations and route every data-sized append through
appendAll so the bulk-spread limit still holds.
2026-11-10 02:18:34 +00:00
Kaoru0822andClaude Opus 5.5 3b9b461924 fix(lint): see through type-only wrappers around bulk-append callees
rea/no-bulk-append-spread unwrapped only optional chains, so
`target.push!(...items)`, `(target.push as T)(...items)`, satisfies and
angle-bracket assertions reached the call with a non-member callee and
were not reported. Strip TypeScript wrapper and grouping nodes before
matching the method, and cover those forms and wrapped receivers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 17:03:52 +00:00
Kaoru0822 7d5a459987 Merge remote-tracking branch 'origin/main' into fix/8545-bulk-append-overflow 2026-11-01 17:02:31 +00:00
Kaoru0822andClaude Opus 5.5 fb566c800c fix(analysis): append large inventories without exceeding call-argument limits
Spreading a data-sized collection into push, splice, Math.max/min or a
variadic helper turns each element into a call argument. Above roughly
125,000 elements V8 runs out of stack, so admitted inputs failed with
RangeError: managed/native verification exited with execution_failure,
import-reference-source with UNKNOWN, and DirectoryArtifactReader and
JavaScript export-binding selection threw.

Add appendAll in src/domain/arrayAppend.ts, which appends in place and in
order without expanding elements into arguments, and use it at every
production push/splice spread site. Replace the data-sized Math.max/min and
variadic loader-queue spreads with loops or array parameters. Add the
rea/no-bulk-append-spread lint rule so the pattern cannot return; the few
structurally bounded spreads keep disable comments naming their bound.

Fixes #8545

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-01 16:53:16 +00:00
537 changed files with 7724 additions and 13194 deletions

No files matched your search

+3 -14
View File
@@ -25,7 +25,7 @@ jobs:
node-version-file: .nvmrc
- name: Validate the SDK pin against the provider's verified release
run: node scripts/verify-ghidra-bridge-sources.mjs --check-sdk-pin >> "$GITHUB_ENV"
# The bridge probe imports only source-owned ARM, Thumb and MIPS bytes and never
# The bridge probe imports only source-owned ARM/Thumb bytes and never
# executes target code. All SDK/project state stays in runner storage.
- name: Acquire checksum-verified Ghidra SDK in isolated runner storage
shell: bash
@@ -46,7 +46,7 @@ jobs:
java_home_variable="JAVA_HOME_${GHIDRA_JAVA_MAJOR}_X64"
test -n "${!java_home_variable}"
JAVA_HOME="${!java_home_variable}" npm run verify:ghidra:bridge-sources -- --verified-sdk
- name: Inspect real ARM, Thumb and MIPS instruction context through the bridge
- name: Inspect real ARM and Thumb instruction context through the bridge
shell: bash
run: |
java_home_variable="JAVA_HOME_${GHIDRA_JAVA_MAJOR}_X64"
@@ -58,21 +58,10 @@ jobs:
-import "${probe_root}/arm-thumb.bin" -loader BinaryLoader -processor ARM:LE:32:v7 \
-noanalysis -readOnly -deleteProject \
-scriptPath "${GITHUB_WORKSPACE}/tests/conformance/ghidra;${GITHUB_WORKSPACE}/bridge/ghidra" \
-postScript ReaInstructionContextProbe.java "${GITHUB_WORKSPACE}/bridge/ghidra" \
-postScript ReaArmContextProbe.java "${GITHUB_WORKSPACE}/bridge/ghidra" \
> "${probe_root}/output.log" 2>&1 || { cat "${probe_root}/output.log"; exit 1; }
cat "${probe_root}/output.log"
grep -F 'REA_ARM_CONTEXT_OK arm=0 thumb=1 unavailable=null (injected provider value)' "${probe_root}/output.log"
mkdir -p "${probe_root}/mips-project"
# MIPS32 addiu v0,a0,3; MIPS16 li v0,90. These bytes are never executed.
printf '\003\000\202\044\132\152\000\000' > "${probe_root}/mips.bin"
timeout 180s "${GHIDRA_INSTALL_DIR}/support/analyzeHeadless" "${probe_root}/mips-project" MipsContext \
-import "${probe_root}/mips.bin" -loader BinaryLoader -processor MIPS:LE:32:default \
-noanalysis -readOnly -deleteProject \
-scriptPath "${GITHUB_WORKSPACE}/tests/conformance/ghidra;${GITHUB_WORKSPACE}/bridge/ghidra" \
-postScript ReaInstructionContextProbe.java "${GITHUB_WORKSPACE}/bridge/ghidra" mips \
> "${probe_root}/mips-output.log" 2>&1 || { cat "${probe_root}/mips-output.log"; exit 1; }
cat "${probe_root}/mips-output.log"
grep -F 'REA_MIPS_CONTEXT_OK standard=0 mips16=1' "${probe_root}/mips-output.log"
# Each probe imports source-owned bytes at 0x1000, away from the Cortex-M
# vector symbols that ARMCortex.pspec defines at 0, and decodes them from
# the language's default context, so M-profile Thumb state must hold at
+2
View File
@@ -76,12 +76,14 @@
{
"files": ["src/**/*.ts"],
"rules": {
"rea/no-bulk-append-spread": "error",
"typescript/no-misused-promises": "error"
}
},
{
"files": ["tests/**/*.ts", "src/**/*.test.ts", "src/**/*.fixture.ts"],
"rules": {
"rea/no-bulk-append-spread": "off",
"max-lines": "off",
"max-lines-per-function": [
"error",
-4
View File
@@ -619,10 +619,6 @@ answer or missing marked output reports a reader error; it does not establish
that the document lacks the expected value. Correct the command or its output and retry; increase
`timeout_s` within its accepted range when the reader needs longer.
A source inspection failure, such as an inaccessible parent or a symlink loop,
also reports a reader error and leaves affected checks undecided. Only a missing
source or a source that is not a regular file is reported as absent.
Export runs readers on both retained ends, using the seed bytes as the bundle
actually ships them. The witness and exported verifier run them in the
computer after the app's files settle. Cleanup failures retain the reader's
+9
View File
@@ -187,6 +187,15 @@ operation names, schemas or types; display-only selection remains allowed.
`rea/no-fixture-root-cleanup` follows `const` fixture-root aliases into teardown
deletion; mutable bindings and mixed root arrays remain outside its scope.
`rea/no-bulk-append-spread` rejects spread arguments to `push`, `unshift` and
`splice` in production sources. Each element becomes a call argument, and V8
bounds argument count by stack size (about 125,000 on a default main-thread
stack), so a data-sized collection throws `RangeError`. Use `appendAll` from
`src/domain/arrayAppend.ts`, or name the structural bound in a disable comment.
The rule does not check receiver types or other variadic calls such as
`Math.max` and `String.fromCharCode`. Reproduce such limits in a forked test
project: worker threads default to a larger stack.
`rea/no-shared-module-mock` resolves Vitest import bindings before rejecting
module mocks in projects without per-file isolation. The test-project definitions
own isolation for Vitest and lint; the executable gate checks the resolved policy
-32
View File
@@ -93,38 +93,6 @@ include the smallest useful reproduction, impact and coverage limits. Do not
claim autonomous-agent selection from a scripted trace. Check related reports
before proposing another issue, and avoid treating lack of API parity as a bug.
### Write descriptions for selection and a valid first call
Start with the analyst task, selected source and useful result. Put material
effects and the deciding boundary between neighboring tools early. Progressive
discovery previews the first 240 characters, so those characters should identify
the operation without requiring the rest of its reference documentation.
Keep exact call syntax where the advertised schema cannot convey it. The compact
input profile strips most annotation prose and can reduce nested controls to
`{type: "object"}`. Moving selectors into parameter descriptions alone does not
make them available to those clients. For example, retained pseudocode needs
both `source.kind: "retained-evidence"` and `view: {kind: "native", facet:
"pseudocode"}`; a short exact example can be more useful than many paragraphs.
Use the producer's handoff for other supported views rather than maintaining
another supposedly exhaustive list in the shared consumer description.
Keep configuration tutorials and detailed matching algorithms with their owning
parameter contracts, product guides and actionable failures. Retain applicability,
source identity, uncertainty, authority and cleanup facts needed to choose or
safely continue the operation. An unresolved symbol lookup is not a completed
reference search; an exhausted page does not establish absence in unexamined
source. Shorter copy must preserve these distinctions.
For wording changes, use a small set of representative agent tasks at the affected
boundaries. Hold schemas, tool order, fixtures and host configuration fixed when
comparing copy; add repetitions only for failures or ambiguous outcomes. Score
actual arguments, corrective calls and supported final claims separately. A
correct eventual answer can conceal malformed-call loops, while a valid call
can still produce an overstated answer. Report whole-task usage, including
retries and cached input, rather than treating fewer description bytes as proof
of lower token cost.
## Design recovery around the failure
Distinguish a request that violates its input contract from a valid selection
-22
View File
@@ -1413,28 +1413,6 @@ Evidence and missing source records, see
Other outputs have their own receipt shapes; for example,
[environment export](#export-an-environment-bundle) reports a published directory.
A failure that carries partial Evidence in `details.partial_observation`, such
as `provider_timeout` or `resource_constraint`, publishes that partial Evidence
to `--evidence_output` under the same new-file and atomic rules, then keeps its
error code, message and remediation when cleanup is confirmed. With `--detail summary`,
`details.partial_observation` holds the summary view of the partial Evidence
instead of the complete record, and `details.diagnostics.evidence_bundle`
reports the written path and byte count. Linked source records must all be
available before publication; missing sources leave the destination unchanged
and are reported in `details.source_closure`. A provider cleanup failure can
still publish its captured Evidence without clearing the cleanup error or its
resource ownership. If publication is refused before commit, for example because
the file already exists, the complete partial Evidence stays inline and `details.diagnostics.evidence_bundle_error` gives the
refusal's code, message and a limitation. A refusal after commit keeps the
receipt in `details.diagnostics.evidence_bundle` and the delivered view: when
staging cleanup stays incomplete, the code becomes `cleanup_incomplete`, the
original failure is retained in `details.execution_failure`, and `details` also carries `published`,
`published_path`, `bytes`, `cleanup: "incomplete"` and `resources`, as for a
successful run, and a later cancellation reports `cancelled` in
`evidence_bundle_error` with the signal's exit status. Without
`--evidence_output`, and for cancellation before publication or raw provider
observations, the failure output is unchanged.
When the reader of piped output closes early, as `rea ... | head` does, the
CLI stops writing, prints no error and exits `141` (`128 + 13`, the status of a
SIGPIPE-terminated writer). The operation itself may have completed; only its
@@ -317,9 +317,6 @@ MCP returns a `partial_observation` retained reference after recording succeeds;
open it with `inspect_analysis_view` on the same connection. If the SDK stopped
waiting before receiving the reply, follow [MCP cancellation recovery](../../reference/mcp-contracts.md#progress-and-cancellation)
to find the record. CLI failure output contains the complete partial Evidence.
With `--evidence_output`, the CLI also publishes it to that file and, with
`--detail summary`, prints only its summary view; see
[evidence publication](../../getting-started/cli.md#output-and-exit-status).
Owned analyzer cleanup is verified before private files are released; uncertain
cleanup retains ownership for retry and is reported separately.
Typed cancellation or another primary interruption remains attached while
+3 -8
View File
@@ -32,14 +32,9 @@ otherwise.
followed by a version token beginning with a digit and ends at a NUL or
newline within 512 bytes. `version` is that token. `builder` is the first
parenthesized group (`user@host`). `compiler` is the second group up to the
last `) #`, so nested parentheses in compiler strings are kept. Since Linux
6.1, kbuild also leaves the weak banner of `init/version.o`. It is compiled
before the build number and timestamp exist, so unless `KBUILD_BUILD_VERSION`
and `KBUILD_BUILD_TIMESTAMP` are both set, it has a space right after `#` or at
its end. When a later banner repeats that text up to `#` without that gap, the
later banner is reported; otherwise the first one is. The banner is `absent`
when no such string was found in the searched bytes and `unknown` when the
bytes were not searched.
last `) #`, so nested parentheses in compiler strings are kept. The banner is
`absent` when no such string was found in the searched bytes and `unknown`
when the bytes were not searched.
`config` is `present` only when all of the following hold:
+1 -4
View File
@@ -62,10 +62,7 @@ arguments are present, argument character length, target extension, whether
`ShowCommand` is minimized, and whether the icon location differs from the
recorded target path. That comparison is `null` unless the icon and a recorded
target are both full paths: a stored filename, relative path, or `%VAR%` form
cannot show that two locations differ. Windows stores the `LinkInfo` path only
as ANSI when the system code page can represent it. An ANSI path with non-ASCII
bytes is therefore not compared with a Unicode icon location, or the reverse,
because the code page is unobserved. This workflow does not emit verdicts
cannot show that two locations differ. This workflow does not emit verdicts
about intent.
A missing optional section is observed absence of that structure. It is not
+2 -54
View File
@@ -60,48 +60,6 @@ Evidence. `build_call_path.functions` also accepts retained references to the
complete dossiers. Request `detail: "complete"` only when the record must travel
inline.
The `inspect_analysis_view` description gives common call shapes rather than an
exhaustive second catalog of view families. Follow the producing tool's named
view kind and selector handoff. A summary's `normalized_result.parent_evidence_id`
names the complete parent, not the summary record itself. Preserve that ID and
the source discriminator:
```json
{
"source": {
"kind": "retained-evidence",
"evidence_id": "ev_<parent digest>"
},
"view": {
"kind": "native",
"facet": "pseudocode",
"offset": 0,
"limit": 128
}
}
```
The same source form works for a compatible inventory occurrence page with
`view: {"kind":"inventory","collection":"occurrences","offset":0,"limit":64}`,
a saved network record page with
`view: {"kind":"capture","detail":"preview","offset":0,"limit":20}`, or a
managed method page with
`view: {"kind":"managed-members","collection":"methods","offset":0,"limit":64}`.
`view: {"kind":"summary"}` has no facet. These are examples, not all supported
families; use the producer and its guide for collection-specific fields and
selection. The full input schema exposes accepted variants, while the default
compact profile can omit nested shapes and parameter descriptions. Refreshing
the same compact catalog does not restore those missing shapes; do not invent
selectors from an opaque object declaration.
Text pages use UTF-16 code units; row pages count the selected retained
collection. Source coverage and retained-page coverage remain distinct. A
source's null or unavailable collection stays unknown, while an exhausted
partial inventory does not prove that unexamined branches contain no matches.
Likewise, a returned text page does not establish the whole parent's length
unless the result reports that extent or exhaustion. Paging reuses completed
Evidence and does not establish current live state.
Saved records from the pre-6.6 identity projection that omitted
`relationship_coverage: null` from its digest remain incompatible, even when
the serialized field is present. REA recognizes that exact previous commitment
@@ -618,23 +576,13 @@ batch-workload limit tracked in issue #5930.
### Error recovery and deadline ownership
Initialization instructions retain the deadline distinction before a host can
discard a tool response; detailed recovery stays with the failure that owns it.
Invalid input calls should be corrected from the connected schema
Initialization instructions and the static JavaScript tool description teach
recovery before a host can discard a tool response. Invalid input calls should be corrected from the connected schema
(refresh `tools/list` only if stale), not retried unchanged. REA's returned
`provider_timeout` describes its server/provider deadline; provider-authored
remediation names the applicable controls and preserves partial Evidence and
cleanup uncertainty.
An unresolved symbol lookup is a valid result, not a malformed request. It does
not establish that a reference search completed or that the binary lacks the
feature. Preserve the exact selector and the provider inventory's coverage.
When remediation names an editable setting and its bounds, call
`update_analysis_settings` directly if advertised; inspecting those same bounds
first is optional. Explicit per-call values override runtime defaults, so fix
the selected argument when remediation identifies it. Runtime updates apply to
new work and cannot release retained cleanup resources.
A host timeout without a REA result is different: execution and cleanup remain
unknown. Increasing an advertised `analysis_timeout_ms` cannot fix a shorter
client deadline. Configure the client's tool timeout with execution, cleanup
-1
View File
@@ -26,7 +26,6 @@ for new work, roadmap granularity and producer-owned contracts.
- [Native call source coordinates](native/call-source-locations/spec.md) ([plan](native/call-source-locations/plan.md), [tasks](native/call-source-locations/tasks.md), [verification](native/call-source-locations/verification.md))
- [Go static string-header literal recovery](native/go-string-literals/spec.md) ([plan](native/go-string-literals/plan.md), [tasks](native/go-string-literals/tasks.md))
- [Windows Shell Link shortcut inspection](native/windows-shortcut/spec.md) ([plan](native/windows-shortcut/plan.md), [tasks](native/windows-shortcut/tasks.md))
- [NTFS $MFT and USN Journal inspection](native/ntfs-mft/spec.md) ([plan](native/ntfs-mft/plan.md), [tasks](native/ntfs-mft/tasks.md))
- [Solidity terminal metadata inspection](solidity/cbor-metadata/spec.md) ([plan](solidity/cbor-metadata/plan.md), [tasks](solidity/cbor-metadata/tasks.md), [verification](solidity/cbor-metadata/verification.md))
- [Inspect a V8 code cache container](v8/code-cache-inspection/spec.md) ([plan](v8/code-cache-inspection/plan.md), [tasks](v8/code-cache-inspection/tasks.md))
- [Deno compile executable inventory and extraction](artifacts/deno-standalone/spec.md) ([plan](artifacts/deno-standalone/plan.md), [tasks](artifacts/deno-standalone/tasks.md))
@@ -26,10 +26,7 @@ these files.
read through the stable artifact read and never executed.
2. The banner is the first printable `Linux version ` string followed by a
version token that starts with a digit and ends at a NUL or newline within
512 bytes. Version, builder and compiler are split from it. A banner with a
space right after `#` or at its end is the kbuild placeholder from
`init/version.o`, built without a build number or timestamp; a later banner
that repeats its text up to `#` without that gap replaces it. The scan is
512 bytes. Version, builder and compiler are split from it. The scan is
linear in the searched bytes.
3. A config is `present` only when `IKCFG_ST` is followed by a gzip member that
ends exactly at the next `IKCFG_ED`, inflates within `max_config_bytes`,
-39
View File
@@ -1,39 +0,0 @@
# Implementation plan
## Boundary and contract
Follow the existing artifact readers. `src/domain/windows/ntfsMft.ts` owns FILE
record bytes and coverage; `src/domain/artifactFormatMetadata.ts` owns the
`ntfs-mft` result schema. `NtfsMftArtifactReader` reads the admitted file
through `ArtifactFileSource` and exposes its facts through `formatMetadata()`.
A record stream is its own content, so the root occurrence carries every fact
and no members are enumerated.
Recognition lives in `hasMftFileRecordSignature` and is applied in
`classifyArtifactBytes`, next to the other unambiguous magic probes.
## Incremental work and validation
1. Define the first-step contract, limits and observations; add parser fixtures.
2. Implement the FILE record header and update-sequence fixup parse with
explicit limitations.
3. Add the reader, format descriptor and byte classification.
4. Decode attribute records, then `$FILE_NAME` attributes so a record's parent
reference, names and timestamps become explicit; keep hard links separate.
5. Parse `$UsnJrnl:$J` records and correlate them to records by full file
reference, including sequence numbers, reporting unresolved references.
6. Document the workflow in a native Windows artifact guide.
## Effects and compatibility
The workflow reads one caller-selected local file. It adds no target execution,
injection, mutation, protection recovery, network access, volume mount or live
filesystem access, so no maintainer scope decision is required.
`inspect_artifact` routing for every existing format is unchanged, and the new
format id appears only for bytes whose first record carries the `FILE`
signature.
## References
- https://learn.microsoft.com/en-us/windows/win32/fileio/master-file-table
- https://github.com/libyal/libfsntfs/blob/main/documentation/New%20Technologies%20File%20System%20(NTFS).asciidoc
-51
View File
@@ -1,51 +0,0 @@
# NTFS $MFT and USN Journal inspection
This specification records an implementation increment and its original problem
statement. For current behavior and limitations, see the artifact inventory
guide.
## Problem and outcome
REA has no reader for acquired NTFS metadata artifacts. An analyst holding a
captured `$MFT` stream cannot ask which FILE records it contains, whether their
update-sequence fixups survived acquisition, or which records are directories,
extensions of another record, or no longer in use. Add offline, read-only
inventory of a selected `$MFT` file, delivered in steps so each one merges on
its own.
## Requirements
- Recognize a `$MFT` stream by a plausible first `FILE` record header under
any file name. CLI and MCP target admission route it to the artifact reader.
- Validate each FILE record header: the `FILE` signature, sequence number, flags,
used and total byte counts, base record reference, and the update-sequence
array's offset, count and array bounds.
- Verify the update-sequence fixups: every sector trailer the array covers must
hold the array's sequence number. A mismatch keeps the record's header facts
and is reported per record and in the metadata count.
- Report a missing, misplaced or short update-sequence array as unverified
rather than as a clean record, and a trailing partial record as a byte count.
- Report the first invalid record signature and the number of invalid slots.
Continue at the next slot when the stream declared a supported record size.
With a fallback record size, stop at the first invalid signature.
- Bound the whole file at a fixed 512 MiB ceiling and report the ceiling as a
`limit` failure with its boundary.
- Return the first 32 decoded record headers with their source offsets and the
number of omitted headers. Aggregate counts cover the full parsed stream.
- Parse only the records' headers and fixups. Attributes, filenames, data
streams and the `$UsnJrnl:$J` journal are not decoded in this step, so no path,
file size, file reference correlation or change history is claimed.
## Acceptance
Synthesized fixtures cover a clean record, a failed fixup, a missing update
sequence array, an array shorter than the record's sector count, extension and
directory records, a record whose signature is wrong mid-stream, a trailing
partial record, an undeclared record size, a 4 KiB record, a stream shorter than
one header, bytes without the `FILE` signature, and the file-size ceiling.
Inventory tests cover the root occurrence's metadata and the coverage status
when a fixup fails, and classification tests cover several file names for the
same bytes.
Not covered in this step: real `$MFT` files written by a Windows host, USN
journal records, attribute decoding, and any correlation between the two.
-11
View File
@@ -1,11 +0,0 @@
# Tasks
- [x] Define the first-step scope, contract and limits.
- [x] Parse FILE record headers and update-sequence fixups with synthesized
fixtures and explicit limitations.
- [x] Add the reader, format descriptor and byte classification.
- [ ] Decode attribute records, then `$FILE_NAME` attributes with parent
references, names, hard links and timestamps.
- [ ] Parse `$UsnJrnl:$J` records and correlate them to records by full file
reference.
- [ ] Verify against real `$MFT` and journal captures from a Windows host.
-3
View File
@@ -11,9 +11,6 @@
"src/composition/webBundleAnalysisWorkerProcess.ts",
"src/contracts/errorSchemas.ts",
"scripts/verify/fridaBindingsCompatibility.ts",
"scripts/verify/analysis/flutter-budget-calibration/task.mjs",
"scripts/verify/analysis/javascript-precision-calibration/scan.mjs",
"scripts/verify/analysis/source-location-delivery/task.mjs",
"scripts/verify/termux/browser.mjs",
"scripts/verify-real-ghidra-noreturn.mjs",
"scripts/verify-real-ghidra-decompiler-failure.mjs",
-1
View File
@@ -76,7 +76,6 @@
"bridge/agentenv/solve_cleanup.py",
"scripts/rea.mjs",
"scripts/lib/cli-process-environment.mjs",
"scripts/lib/runtime-admission.mjs",
"scripts/rea-android-workspace-guardian.mjs",
"scripts/electron-active-hook.cjs",
"scripts/electron-active-hook-boundaries.cjs",
+1 -5
View File
@@ -247,8 +247,6 @@ const PATH_SCOPES = {
"tests/boundary/filesystem/releaseProposal.test.ts",
"scripts/rea.mjs",
"scripts/lib/cli-process-environment.mjs",
"scripts/lib/runtime-admission.mjs",
"scripts/lib/runtime-admission.d.mts",
"install.sh",
"src/config/**",
"src/application/Setup*.ts",
@@ -495,8 +493,6 @@ const PATH_SCOPES = {
"src/contracts/toolEffects.ts",
"scripts/rea.mjs",
"scripts/lib/cli-process-environment.mjs",
"scripts/lib/runtime-admission.mjs",
"scripts/lib/runtime-admission.d.mts",
"src/process/**",
"src/artifacts/SafeOutputTree.ts",
"scripts/verify/javascript/recovery.mjs",
@@ -636,7 +632,7 @@ const PATH_SCOPES = {
"src/ghidra/GhidraJavaProperties.ts",
"src/ghidra/GhidraReleasePolicy.ts",
"scripts/verify-ghidra-bridge-sources.mjs",
"tests/conformance/ghidra/ReaInstructionContextProbe.java",
"tests/conformance/ghidra/ReaArmContextProbe.java",
"tests/conformance/ghidra/ReaComputedCallCoverageProbe.java",
"tests/conformance/ghidra/ReaDecodeProbe.java",
".github/workflows/ghidra-bridge-sources.yml",
@@ -1,7 +0,0 @@
/** Remove copied authentication independently; retain ordinary files until commands release. */
export function closeAgentEvaluation(options: {
readonly commands: { close(): Promise<void> };
readonly authPath: string;
readonly evaluationRoot: string;
readonly keepFixtures: boolean;
}): Promise<void>;
-24
View File
@@ -1,24 +0,0 @@
import { rm } from "node:fs/promises";
import { closeVerifierResources } from "./verifier-cleanup.mjs";
/** Remove copied authentication independently; retain ordinary files until commands release. */
export async function closeAgentEvaluation({
commands,
authPath,
evaluationRoot,
keepFixtures,
}) {
let commandsReleased = false;
await closeVerifierResources([
async () => {
await commands.close();
commandsReleased = true;
},
() => rm(authPath, { force: true }),
async () => {
if (commandsReleased && !keepFixtures)
await rm(evaluationRoot, { recursive: true, force: true });
},
]);
}
@@ -375,15 +375,11 @@ test("dispatch reverses origin completion; client and capture may arrive in othe
test("cleanup verifies and retires a separately owned producer group", async () => {
const root = await mkdtemp(join(tmpdir(), "rea-http3-group-"));
const runId = randomUUID();
const child = spawn(
process.execPath,
["--eval", "setInterval(() => {}, 1000)"],
{
detached: true,
env: { ...process.env, REA_PROCESS_RUN_ID: runId },
stdio: "ignore",
},
);
const child = spawn("/usr/bin/sleep", ["60"], {
detached: true,
env: { ...process.env, REA_PROCESS_RUN_ID: runId },
stdio: "ignore",
});
let primaryError;
let cleaned = false;
try {
-1
View File
@@ -24,7 +24,6 @@ export interface RealHopperFixtureTargets {
readonly manifestPath: string;
readonly primary: RealHopperFixtureTarget;
readonly secondary: RealHopperFixtureTarget;
readonly unicode?: RealHopperFixtureTarget;
readonly large: RealHopperFixtureTarget;
readonly oracle: RealHopperFixtureOracle;
readonly largeOracle: RealHopperLargeFixtureOracle;
+2 -1
View File
@@ -34,8 +34,9 @@ def note_header(content, note, **changes):
# Authored metadata carriers exercise actual parser projection through public
# inspection, independently of the host that produced the captured amd64 core.
# The named ARCHEXT/UNWIND carriers are written once below using numeric type IDs.
for name, machine, kind in [
("aarch64-archext", "EM_AARCH64", "PT_AARCH64_ARCHEXT"),
("aarch64-unwind", "EM_AARCH64", "PT_AARCH64_UNWIND"),
("aarch64-mte-opaque", "EM_AARCH64", 0x70000002),
("amd64-processor-opaque", "EM_X86_64", 0x70000001),
]:
+26 -33
View File
@@ -1,22 +1,19 @@
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { createVerifierMcpOwner } from "./verifier-mcp-owner.mjs";
import { withVerifierCleanup } from "./verifier-cleanup.mjs";
import { execFile } from "node:child_process";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { FULL_CATALOG_IDENTITY } from "../../dist/catalogIdentity.js";
import { verifyCompleteToolCatalog } from "./verify-package-core.mjs";
/** Install the selected archive or registry specifier with an owned ten-minute deadline. */
export async function installCanaryPackage(
specifier,
workspace,
environment,
commands,
) {
const execute = promisify(execFile);
/** Install the selected archive or registry specifier before any protocol deadline. */
export async function installCanaryPackage(specifier, workspace, environment) {
const prefix = join(workspace, "installed");
await commands.exec(
await execute(
"npm",
[
"install",
@@ -29,12 +26,7 @@ export async function installCanaryPackage(
prefix,
specifier,
],
{
cwd: workspace,
env: environment,
timeout: 600000,
maxBuffer: 16 * 1024 * 1024,
},
{ cwd: workspace, env: environment, maxBuffer: 16 * 1024 * 1024 },
);
return join(prefix, "node_modules", "rea-agents");
}
@@ -44,7 +36,7 @@ export async function verifyInstalledCanaryPackage(
packageRoot,
version,
workspace,
{ environment, commands },
environment,
) {
const installed = JSON.parse(
await readFile(join(packageRoot, "package.json"), "utf8"),
@@ -55,10 +47,9 @@ export async function verifyInstalledCanaryPackage(
);
const launcher = join(packageRoot, "scripts", "rea.mjs");
for (const option of ["--help", "--llms"])
await commands.exec(process.execPath, [launcher, option], {
await execute(process.execPath, [launcher, option], {
cwd: workspace,
env: environment,
timeout: 30000,
maxBuffer: 16 * 1024 * 1024,
});
const transport = new StdioClientTransport({
@@ -74,20 +65,22 @@ export async function verifyInstalledCanaryPackage(
if (stderr.length < 16_384)
stderr += chunk.toString("utf8").slice(0, 16_384 - stderr.length);
});
const owner = createVerifierMcpOwner();
commands.adopt(owner, "installed package MCP transport");
return withVerifierCleanup(async () => {
try {
await client.connect(transport);
const toolNames = await verifyCompleteToolCatalog(client);
return {
mcpTools: toolNames.length,
canonicalMcpTools: FULL_CATALOG_IDENTITY.counts.mcp_tools,
};
} catch (cause) {
throw new Error(`Installed package MCP verification failed: ${stderr}`, {
cause,
});
} finally {
try {
await owner.open(client, transport);
const toolNames = await verifyCompleteToolCatalog(client);
return {
mcpTools: toolNames.length,
canonicalMcpTools: FULL_CATALOG_IDENTITY.counts.mcp_tools,
};
} catch (cause) {
throw new Error(`Installed package MCP verification failed: ${stderr}`, {
cause,
});
await client.close();
} finally {
await transport.close();
}
}, [() => owner.close()]);
}
}
-11
View File
@@ -1,11 +0,0 @@
/** True when `version` is a release admitted by an `engines.node` range. */
export function satisfiesNodeRange(range: string, version: string): boolean;
/** Refusal text for a release outside `range`, or undefined when it is admitted. */
export function unsupportedRuntimeMessage(
range: string,
version: string,
): string | undefined;
/** Human-readable release families for an `engines.node` range. */
export function supportedReleaseProse(range: string): string;
-54
View File
@@ -1,54 +0,0 @@
/**
* Reject a Node.js release the compiled runtime cannot link against.
*
* The dispatcher imports compiled modules before it can report anything, and
* those modules import Node APIs that some releases do not export. Checking the
* published range here keeps that failure a readable refusal instead of a
* module-linking stack trace.
*
* `range` is the package's own `engines.node`, so the dispatcher and the
* installer cannot drift apart. This module deliberately has no imports: the
* dispatcher must admit the runtime before any dependency, including `semver`,
* can be resolved.
*/
export const unsupportedRuntimeMessage = (range, version) => {
if (typeof range !== "string" || range.length === 0) return undefined;
if (satisfiesNodeRange(range, version)) return undefined;
return (
`REA requires ${supportedReleaseProse(range)}; this process runs Node.js ${version}. ` +
"Install a supported release, then retry."
);
};
/** Human-readable release families for an `engines.node` range. */
export const supportedReleaseProse = (range) =>
`Node.js ${range
.split("||")
.map((clause) => {
const floor = /^(\^|>=)\s*(\d+\.\d+\.\d+)$/.exec(clause.trim());
if (floor === null) return clause.trim();
return floor[1] === ">="
? `${floor[2]}+`
: `${floor[2].split(".")[0]}.x (>=${floor[2]})`;
})
.join(", ")}`;
const VERSION = /^(\d+)\.(\d+)\.(\d+)$/;
const FLOOR = /^(>=|\^)\s*(\d+)\.(\d+)\.(\d+)$/;
/** Every clause of the published range is `^major.minor.patch` or `>=`. */
export const satisfiesNodeRange = (range, version) => {
const found = VERSION.exec(version);
// A prerelease or a non-release spelling is never an admitted release.
if (found === null) return false;
const [, major, minor, patch] = found;
return range.split("||").some((clause) => {
const floor = FLOOR.exec(clause.trim());
if (floor === null) return false;
const [, operator, floorMajor, floorMinor, floorPatch] = floor;
if (Number(major) !== Number(floorMajor))
return operator === ">=" && Number(major) > Number(floorMajor);
if (minor !== floorMinor) return Number(minor) > Number(floorMinor);
return Number(patch) >= Number(floorPatch);
});
};
+2
View File
@@ -1,6 +1,7 @@
import noBufferDeepEquality from "./no-buffer-deep-equality.mjs";
import cancellationCause from "./cancellation-cause.mjs";
import noBulkAppendSpread from "./rules/no-bulk-append-spread.mjs";
import noEvidenceJsonPreadmission from "./rules/no-evidence-json-preadmission.mjs";
import noFixtureRootCleanup from "./rules/no-fixture-root-cleanup.mjs";
import noPositionalToolContract from "./rules/no-positional-tool-contract.mjs";
@@ -11,6 +12,7 @@ export default {
rules: {
"no-buffer-deep-equality": noBufferDeepEquality,
"cancellation-cause": cancellationCause,
"no-bulk-append-spread": noBulkAppendSpread,
"no-evidence-json-preadmission": noEvidenceJsonPreadmission,
"no-fixture-root-cleanup": noFixtureRootCleanup,
"no-positional-tool-contract": noPositionalToolContract,
@@ -0,0 +1,3 @@
import type { RuleTester } from "oxlint/plugins-dev";
declare const rule: Parameters<RuleTester["run"]>[1];
export default rule;
@@ -0,0 +1,57 @@
const bulkMethods = new Set(["push", "unshift", "splice"]);
/** Syntax that leaves the evaluated callee or receiver unchanged. */
const transparent = new Set([
"ChainExpression",
"ParenthesizedExpression",
"TSAsExpression",
"TSNonNullExpression",
"TSSatisfiesExpression",
"TSTypeAssertion",
"TSInstantiationExpression",
]);
const unwrap = (node) => {
while (node && transparent.has(node.type)) node = node.expression;
return node;
};
const propertyName = (member) => {
if (!member.computed && member.property.type === "Identifier")
return member.property.name;
if (member.computed && member.property.type === "Literal")
return member.property.value;
return undefined;
};
export default {
meta: {
type: "problem",
docs: {
description:
"Append collections element-wise instead of spreading them into call arguments.",
},
schema: [],
messages: {
spread:
"Spreading into {{method}}() makes each element a call argument, and engines bound argument count by stack size, so a data-sized collection throws RangeError. Use appendAll from src/domain/arrayAppend.ts, or document the structural bound in a disable comment.",
},
},
create(context) {
return {
CallExpression(node) {
const callee = unwrap(node.callee);
if (callee?.type !== "MemberExpression") return;
const method = propertyName(callee);
if (!bulkMethods.has(method)) return;
for (const argument of node.arguments)
if (argument.type === "SpreadElement")
context.report({
node: argument,
messageId: "spread",
data: { method },
});
},
};
},
};
-1
View File
@@ -46,7 +46,6 @@ if (await pathExists(join(root, "src"))) {
} else {
for (const path of [
"scripts/lib/cli-process-environment.mjs",
"scripts/lib/runtime-admission.mjs",
"dist/main.js",
"dist/mcp/main.js",
"dist/cli-runtime/cli.js",
-14
View File
@@ -5,7 +5,6 @@ import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { prepareCliProcessEnvironment } from "./lib/cli-process-environment.mjs";
import { unsupportedRuntimeMessage } from "./lib/runtime-admission.mjs";
// Route production MCP before importing Incur. Incur owns registration helpers
// such as `mcp add`, while the MCP runtime serves the tool catalog over stdio
@@ -14,19 +13,6 @@ const args = process.argv.slice(2);
const { default: packageJson } = await import("../package.json", {
with: { type: "json" },
});
// Admit the runtime before the first compiled import. Compiled modules import
// Node APIs that absent releases only reject while linking, so an unsupported
// release previously surfaced as a module error from every command, including
// `--version` and `doctor`. `process.exit` is required here: setting exitCode
// would still reach the imports below.
const unsupportedRuntime = unsupportedRuntimeMessage(
packageJson.engines?.node,
process.versions.node,
);
if (unsupportedRuntime !== undefined) {
process.stderr.write(`${unsupportedRuntime}\n`);
process.exit(1);
}
prepareCliProcessEnvironment(
process.env,
process.platform,
+15 -9
View File
@@ -1,4 +1,3 @@
import { closeAgentEvaluation } from "./lib/agent-evaluation-cleanup.mjs";
import { importVerifierSource } from "./lib/verifier-source-loader.mjs";
import { createHash } from "node:crypto";
@@ -8,6 +7,7 @@ import {
mkdir,
mkdtemp,
readFile,
rm,
writeFile,
} from "node:fs/promises";
import { homedir } from "node:os";
@@ -18,7 +18,10 @@ import { tsImport } from "tsx/esm/api";
import { PRODUCT_IDENTITY } from "../dist/identity.js";
import { createAgentEvaluationCommands } from "./lib/agent-evaluation-commands.mjs";
import { assertAgentEvaluationReportPassed } from "./lib/agent-evaluation-report-gate.mjs";
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
import {
closeVerifierResources,
withVerifierCleanup,
} from "./lib/verifier-cleanup.mjs";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
const { parseSkillManifestMetadata } = await importVerifierSource(
@@ -424,13 +427,16 @@ await withVerifierCleanup(
},
// Even a deliberately retained fixture directory must never retain auth.
[
() =>
closeAgentEvaluation({
commands,
authPath,
evaluationRoot,
keepFixtures: process.env.REA_AGENT_EVAL_KEEP_FIXTURES === "true",
}),
async () => {
// A failed process cleanup retains both its exact owner and its files.
await commands.close();
await closeVerifierResources([
() => rm(authPath, { force: true }),
...(process.env.REA_AGENT_EVAL_KEEP_FIXTURES === "true"
? []
: [() => rm(evaluationRoot, { recursive: true, force: true })]),
]);
},
],
);
-27
View File
@@ -6,7 +6,6 @@ import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { absoluteCallerPath } from "../dist/filesystem/callerPath.js";
import { toolContract } from "../dist/contracts/toolContracts.js";
import { il2cppMetadataViewRequestSchema } from "../dist/domain/analysisView/il2cppMetadataView.js";
import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js";
import {
requireMcpOutput,
@@ -80,32 +79,6 @@ await withVerifierCleanup(async () => {
assert.equal(error.code, "resource_constraint");
assert.equal(error.details.reported_limits.boundary, "mcp-response");
assert.match(error.details.reported_limits.evidence_id, /^ev_/u);
const collections = il2cppMetadataViewRequestSchema.options.map(
(option) => option.shape.collection.value,
);
const collection =
["methods", ...collections].find(
(name) => Array.isArray(report[name]) && report[name].length > 0,
) ?? "methods";
const expectedRows = report[collection].slice(0, 16);
const retained = (
await call("inspect_analysis_view", {
source: {
kind: "retained-evidence",
evidence_id: error.details.reported_limits.evidence_id,
},
view: { kind: "il2cpp-metadata", collection, limit: 16 },
})
).normalized_result;
assert.deepEqual(
retained.items,
expectedRows.map((row, index) => ({ ...row, index })),
"Failed complete delivery must retain its exact Evidence before reanalysis",
);
if (expectedRows.length === 0)
console.log(
"[unverified] Retention lookup succeeded, but empty queryable tables provide no nonempty row-content witness",
);
console.log(
`[ok] MCP complete delivery refused at the stdio budget (${error.details.reported_limits.response_bytes_at_least} bytes) with retained Evidence`,
);
+3 -10
View File
@@ -165,9 +165,7 @@ try {
scenario.value.steps.some((step) => step.outcome !== "completed") ||
!scenario.value.steps
.at(-1)
?.after?.nodes.some(
(node) => node.title === "REA fixture incremented 1",
)
?.after?.nodes.some((node) => node.title === "REA fixture incremented")
)
throw new Error(
`Source-owned AX scenario failed: ${JSON.stringify(scenario.value.steps.map(({ outcome, reason }) => ({ outcome, reason })))}`,
@@ -197,12 +195,7 @@ try {
evidence.normalized_result?.steps?.some(
(step) => step.outcome !== "completed",
) ||
evidence.normalized_result?.steps?.length !== 2 ||
!evidence.normalized_result?.steps
?.at(-1)
?.after?.nodes.some(
(node) => node.title === "REA fixture incremented 2",
)
evidence.normalized_result?.steps?.length !== 2
)
throw new Error(
`Native UI CLI did not preserve the selected scenario: ${JSON.stringify(evidence.error ?? evidence.normalized_result?.steps?.map(({ outcome, reason }) => ({ outcome, reason })))}`,
@@ -239,7 +232,7 @@ try {
!captured.steps
.at(-1)
?.after?.nodes.some(
(node) => node.title === "REA fixture incremented 3",
(node) => node.title === "REA fixture incremented",
)
)
throw new Error("MCP did not observe the selected fixture's AX action");
-2
View File
@@ -191,8 +191,6 @@ export async function verifyPackagePack({ root, workspace, commands }) {
throw new Error("package did not expose both rea command entry points");
if (!packedFiles.includes("package/scripts/lib/cli-process-environment.mjs"))
throw new Error("package omitted the CLI process environment owner");
if (!packedFiles.includes("package/scripts/lib/runtime-admission.mjs"))
throw new Error("package omitted the CLI runtime admission owner");
if (packedFiles.some((path) => path.startsWith("package/.agents/")))
throw new Error(
"package included authored skill sources instead of only the generated bundle",
+15 -30
View File
@@ -1,8 +1,8 @@
import { createPackageVerifierCommands } from "./lib/verify-package-commands.mjs";
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
import { execFile } from "node:child_process";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { delimiter, join } from "node:path";
import { promisify } from "node:util";
import { pathToFileURL } from "node:url";
import { lt, prerelease, rcompare, valid } from "semver";
@@ -15,7 +15,7 @@ import {
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
const verifierRun = createVerifierRun();
const execFileAsync = promisify(execFile);
const version = process.argv[2];
if (
typeof version !== "string" ||
@@ -30,33 +30,28 @@ const serverEnvironment = {
npm_config_cache: join(canaryRoot, "npm-cache"),
};
delete serverEnvironment.HOPPER_TARGET_PATH;
const commands = await createPackageVerifierCommands({
workspace: {
path: canaryRoot,
close: () => rm(canaryRoot, { recursive: true, force: true }),
},
});
let surface;
let update;
await withVerifierCleanup(async () => {
try {
const publishedRoot = await installCanaryPackage(
`rea-agents@${version}`,
canaryRoot,
serverEnvironment,
commands,
);
surface = await verifyInstalledCanaryPackage(
publishedRoot,
version,
canaryRoot,
{ environment: serverEnvironment, commands },
serverEnvironment,
);
update = await verifyPublishedUpdate({
version,
canaryRoot,
updaterRoot: publishedRoot,
});
}, [() => commands.close()]);
} finally {
await rm(canaryRoot, { recursive: true, force: true });
}
process.stdout.write(
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), package: "rea-agents", version, ...surface, update })}\n`,
);
@@ -99,7 +94,7 @@ async function verifyPublishedUpdate({
environment,
);
await commands.exec(
await execFileAsync(
"npm",
[
"install",
@@ -109,20 +104,13 @@ async function verifyPublishedUpdate({
prefix,
`rea-agents@${previousVersion}`,
],
{
cwd: canaryRoot,
env: environment,
timeout: 600000,
maxBuffer: 16 * 1024 * 1024,
},
{ cwd: canaryRoot, env: environment, maxBuffer: 16 * 1024 * 1024 },
);
const npmRoot = (
await commands.exec("npm", ["root", "--global"], {
await execFileAsync("npm", ["root", "--global"], {
cwd: canaryRoot,
env: environment,
timeout: 30000,
maxBuffer: 16 * 1024 * 1024,
})
).stdout.trim();
// Older releases may predate the update command. Exercise the new published
@@ -138,10 +126,9 @@ process.stdout.write(JSON.stringify(result));
if (result.status === "failed") process.exitCode = 1;
`,
);
const { stdout } = await commands.exec(process.execPath, [harness], {
const { stdout } = await execFileAsync(process.execPath, [harness], {
cwd: canaryRoot,
env: environment,
timeout: 600000,
maxBuffer: 16 * 1024 * 1024,
});
const result = JSON.parse(stdout);
@@ -165,11 +152,9 @@ if (result.status === "failed") process.exitCode = 1;
);
const help = (
await commands.exec(join(prefix, "bin", "rea-agents"), ["--help"], {
await execFileAsync(join(prefix, "bin", "rea-agents"), ["--help"], {
cwd: canaryRoot,
env: environment,
timeout: 30000,
maxBuffer: 16 * 1024 * 1024,
})
).stdout;
if (!/^\s{2}update\s/mu.test(help) || /^\s{2}upgrade\s/mu.test(help))
@@ -186,10 +171,10 @@ if (result.status === "failed") process.exitCode = 1;
}
async function findPreviousPublishedVersion(targetVersion, cwd, environment) {
const { stdout } = await commands.exec(
const { stdout } = await execFileAsync(
"npm",
["view", "rea-agents", "versions", "--json"],
{ cwd, env: environment, timeout: 30000, maxBuffer: 16 * 1024 * 1024 },
{ cwd, env: environment, maxBuffer: 16 * 1024 * 1024 },
);
const versions = JSON.parse(stdout);
const previous = versions
+17 -26
View File
@@ -1,8 +1,4 @@
#!/usr/bin/env node
import {
closeVerifierResources,
withVerifierCleanup,
} from "./lib/verifier-cleanup.mjs";
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
@@ -50,7 +46,7 @@ transport.stderr?.on("data", (bytes) => {
const ajv = new Ajv2020({ strict: false, validateFormats: false });
const measurements = [];
const contract = toolContract("inspect_web_network_capture");
const report = await withVerifierCleanup(async () => {
try {
try {
await client.connect(transport);
} catch (cause) {
@@ -196,7 +192,7 @@ const report = await withVerifierCleanup(async () => {
});
}
await client.ping();
return {
const report = {
status: "passed",
profile: "aioquic-1.5.0-json-0.3",
measurements,
@@ -206,23 +202,18 @@ const report = await withVerifierCleanup(async () => {
"No PCAP or keylog is read by this verifier; repeated STREAM coverage is not proof of retransmission or network loss.",
],
};
}, [
async () => {
await closeVerifierResources([
() => client.close(),
() => transport.close(),
]);
},
]);
await writeFile(reportPath, JSON.stringify(report, null, 2) + "\n", {
flag: "wx",
mode: 0o600,
});
console.log(
JSON.stringify({
status: report.status,
cases: measurements.length,
report: reportPath,
}),
);
await writeFile(reportPath, JSON.stringify(report, null, 2) + "\n", {
flag: "wx",
mode: 0o600,
});
console.log(
JSON.stringify({
status: report.status,
cases: measurements.length,
report: reportPath,
}),
);
} finally {
await client.close();
await transport.close();
}
+5 -14
View File
@@ -310,6 +310,7 @@ await withVerifierCleanup(async () => {
role: "assistant",
content: "REA_CLIENT_COMPATIBILITY_OK",
};
finalSeen = true;
} else if (skill && !skillRequested) {
skillRequested = true;
message = call(skill.name, { skill: "reverse-engineer-anything" });
@@ -400,13 +401,11 @@ await withVerifierCleanup(async () => {
stderr: join(lab, `${mode}.stderr`),
},
);
const events = response.stdout
const initialized = response.stdout
.split("\n")
.filter(Boolean)
.map((line) => JSON.parse(line));
const initialized = events.find(
(v) => v.type === "system" && v.subtype === "init",
);
.map((line) => JSON.parse(line))
.find((v) => v.type === "system" && v.subtype === "init");
assert(initialized);
const observed = initialized.tools
.filter((n) => n.startsWith("mcp__rea__"))
@@ -418,15 +417,7 @@ await withVerifierCleanup(async () => {
"Qwen must discover the complete REA catalog",
);
assert.equal(fixture.failure, undefined);
const result = events.findLast((event) => event.type === "result");
assert.equal(
result?.subtype,
"success",
"Qwen must emit a successful terminal result",
);
assert.equal(result.is_error, false);
assert.equal(result.result, "REA_CLIENT_COMPATIBILITY_OK");
finalSeen = true;
assert(finalSeen);
if (mode === "call") {
assert(evidenceSeen);
assert(selectedSkillLoaded);
+14 -26
View File
@@ -1,12 +1,8 @@
#!/usr/bin/env node
import {
closeVerifierResources,
withVerifierCleanup,
} from "./lib/verifier-cleanup.mjs";
import assert from "node:assert/strict";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { createPackageVerifierCommands } from "./lib/verify-package-commands.mjs";
import { execFile } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import {
access,
@@ -18,16 +14,17 @@ import {
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { promisify } from "node:util";
import { createServer } from "../dist/server/createServer.js";
const execFileAsync = promisify(execFile);
const cli = resolve("scripts/rea.mjs");
const runCli = async (...arguments_) => {
const { stdout } = await commands.exec(
const { stdout } = await execFileAsync(
process.execPath,
[cli, ...arguments_, "--format", "json"],
{
timeout: 150_000,
encoding: "utf8",
maxBuffer: 4 * 1024 * 1024,
env: process.env,
},
@@ -59,14 +56,8 @@ if (process.env.EXPECTED_PLATFORM !== undefined) {
);
}
const projectRoot = await mkdtemp(join(tmpdir(), "rea-cutter-smoke-"));
const commands = await createPackageVerifierCommands({
workspace: {
path: projectRoot,
close: () => rm(projectRoot, { recursive: true, force: true }),
},
});
const projectPath = join(projectRoot, "smoke.rzdb");
await withVerifierCleanup(async () => {
try {
const discovery = await runCli("list-cutter-sessions");
assert.equal(
discovery.discovery_status,
@@ -120,15 +111,7 @@ await withVerifierCleanup(async () => {
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
const mcpOwner = {
close: () =>
closeVerifierResources([
() => mcpClient.close(),
() => mcpServer.close(),
]),
};
commands.adopt(mcpOwner, "Cutter verifier MCP server");
await withVerifierCleanup(async () => {
try {
await mcpServer.connect(serverTransport);
await mcpClient.connect(clientTransport);
const mcpDiscovery = await mcpClient.callTool({
@@ -155,7 +138,10 @@ await withVerifierCleanup(async () => {
inspection.normalized_result,
"Cutter CLI and MCP returned different analysis results.",
);
}, [() => mcpOwner.close()]);
} finally {
await mcpClient.close();
await mcpServer.close();
}
const annotation = `rea-smoke-${randomUUID()}`;
await runCli(
@@ -226,4 +212,6 @@ await withVerifierCleanup(async () => {
mode: 0o600,
});
console.log(JSON.stringify(report));
}, [() => commands.close()]);
} finally {
await rm(projectRoot, { recursive: true, force: true });
}
+326 -361
View File
@@ -1,7 +1,3 @@
import {
closeVerifierResources,
withVerifierCleanup,
} from "./lib/verifier-cleanup.mjs";
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
@@ -71,428 +67,397 @@ let stderr = "";
transport.stderr?.on("data", (chunk) => {
stderr = (stderr + chunk.toString()).slice(-65536);
});
let connected = false;
let verifierRun;
let report;
await withVerifierCleanup(async () => {
try {
await exec(
ca65,
[join(root, "tests/conformance/ghidra/6502.s"), "-o", object],
{ timeout: 10000 },
try {
await exec(
ca65,
[join(root, "tests/conformance/ghidra/6502.s"), "-o", object],
{ timeout: 10000 },
);
await exec(
ld65,
["-C", join(root, "tests/conformance/ghidra/6502.cfg"), "-o", path, object],
{ timeout: 10000 },
);
const bytes = await readFile(path);
// Independently known opcodes and link addresses from the source fixture.
assert.equal(bytes.toString("hex"), "a907201080c90ad003a92a60a9006018690360");
const sha256 = createHash("sha256").update(bytes).digest("hex");
await client.connect(transport);
const opened = await call("open_binary", {
path,
format: "raw-binary",
architecture: "6502",
entry_point: "0x008001",
provider_id: "ghidra",
});
assert.equal(opened.architecture, "6502");
assert.equal(opened.entry_point, "0x8001");
const status = await call("binary_session");
const image = evidence(await call("inspect_native_load_image"), sha256);
assert.equal(
image.normalized_result.status,
"verified",
JSON.stringify(image.normalized_result),
);
assert.equal(image.normalized_result.architecture, "6502");
assert.equal(image.normalized_result.base_address, "0x8001");
assert.equal(image.normalized_result.language_id, "6502:LE:16:default");
assert.equal(image.normalized_result.compiler_spec_id, "default");
assert.equal(image.normalized_result.file_bytes, bytes.length);
const memory = evidence(
await call("read_bytes", { address: "0x8001", length: bytes.length }),
sha256,
);
assert.equal(memory.normalized_result.bytes_hex, bytes.toString("hex"));
const procedures = evidence(await call("list_procedures"), sha256);
for (const address of ["0x8001", "0x8010"])
assert.ok(
procedures.normalized_result.some(
(procedure) => procedure.address === address,
),
);
const instruction = evidence(
await call("inspect_native_instruction", { address: "0x8003" }),
sha256,
);
assert.equal(instruction.normalized_result.mnemonic.toUpperCase(), "JSR");
assert.ok(
instruction.normalized_result.references.some(
(reference) => reference.call && reference.target_address === "0x8010",
),
);
const pseudo = evidence(
await call("procedure_pseudo_code", { procedure: "0x8010" }),
sha256,
);
assert.match(pseudo.normalized_result, /\+\s*(?:3|0x3|'\\x03')/u);
const filePseudo = evidence(
await call("procedure_pseudo_code", { procedure: "file-offset:0xf" }),
sha256,
);
assert.equal(filePseudo.normalized_result, pseudo.normalized_result);
assert.ok(
filePseudo.locations.some(
(location) => location.kind === "file-offset" && location.offset === 15,
),
);
const rejected = await client.callTool({
name: "open_binary",
arguments: {
path,
format: "raw-binary",
architecture: "6502",
entry_point: "0xffff",
},
});
assert.equal(rejected.isError, true);
assert.match(JSON.stringify(rejected.content), /6502 address width/u);
const preserved = await call("binary_session");
assert.equal(preserved.analysis_run.run_id, status.analysis_run.run_id);
assert.equal(preserved.entry_point, "0x8001");
await call("close_binary");
await client.ping();
const { stdout } = await exec(
process.execPath,
[
entrypoint,
"inspect-native-load-image",
path,
"--target-format",
"raw-binary",
"--architecture",
"6502",
"--entry-point",
"0x8001",
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
);
const cli = parseEvidence(JSON.parse(stdout));
assert.deepEqual(cli.normalized_result, image.normalized_result);
assert.equal(cli.analysis_profile.digest, image.analysis_profile.digest);
assert.equal(cli.subject.digest.sha256, sha256);
const fileCli = await exec(
process.execPath,
[
entrypoint,
"decompile",
path,
"--file-offset=0xf",
"--target-format",
"raw-binary",
"--architecture",
"6502",
"--entry-point",
"0x8001",
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
);
const fileCliEvidence = parseEvidence(JSON.parse(fileCli.stdout));
assert.equal(fileCliEvidence.normalized_result, filePseudo.normalized_result);
assert.deepEqual(fileCliEvidence.locations, filePseudo.locations);
assert.equal(fileCliEvidence.subject.digest.sha256, sha256);
assert.deepEqual(await readFile(path), bytes);
assert.deepEqual(await readdir(runtime), []);
const lowMemory = [];
// BinaryLoader maps file bytes before trying processor-defined blocks. Prove
// that overlapping zero-page/stack definitions never overlay the selected image.
for (const [entry, fullAddressSpace] of [
[0xff, false],
[0, true],
]) {
const lowPath = join(workspace, `low-${entry}.bin`);
await exec(
ld65,
[
"-C",
join(root, "tests/conformance/ghidra/6502.cfg"),
"--start-addr",
String(entry),
"-o",
path,
lowPath,
object,
],
{ timeout: 10000 },
);
const bytes = await readFile(path);
// Independently known opcodes and link addresses from the source fixture.
assert.equal(
bytes.toString("hex"),
"a907201080c90ad003a92a60a9006018690360",
const linked = await readFile(lowPath);
const expectedCode = Buffer.from(
"a907200000c90ad003a92a60a9006018690360",
"hex",
);
const sha256 = createHash("sha256").update(bytes).digest("hex");
await client.connect(transport);
connected = true;
const opened = await call("open_binary", {
path,
expectedCode.writeUInt16LE(entry + 15, 3);
assert.deepEqual(linked, expectedCode);
const lowBytes = fullAddressSpace
? Buffer.concat([linked, Buffer.alloc(65536 - linked.length)])
: linked;
await writeFile(lowPath, lowBytes);
const lowSha256 = createHash("sha256").update(lowBytes).digest("hex");
const lowEntry = `0x${entry.toString(16)}`;
const lowOpened = await call("open_binary", {
path: lowPath,
format: "raw-binary",
architecture: "6502",
entry_point: "0x008001",
entry_point: lowEntry,
provider_id: "ghidra",
});
assert.equal(opened.architecture, "6502");
assert.equal(opened.entry_point, "0x8001");
const status = await call("binary_session");
const image = evidence(await call("inspect_native_load_image"), sha256);
assert.equal(lowOpened.entry_point, lowEntry);
const lowImage = evidence(
await call("inspect_native_load_image"),
lowSha256,
);
assert.equal(
image.normalized_result.status,
lowImage.normalized_result.status,
"verified",
JSON.stringify(image.normalized_result),
JSON.stringify(lowImage.normalized_result),
);
assert.equal(image.normalized_result.architecture, "6502");
assert.equal(image.normalized_result.base_address, "0x8001");
assert.equal(image.normalized_result.language_id, "6502:LE:16:default");
assert.equal(image.normalized_result.compiler_spec_id, "default");
assert.equal(image.normalized_result.file_bytes, bytes.length);
const memory = evidence(
await call("read_bytes", { address: "0x8001", length: bytes.length }),
sha256,
);
assert.equal(memory.normalized_result.bytes_hex, bytes.toString("hex"));
const procedures = evidence(await call("list_procedures"), sha256);
for (const address of ["0x8001", "0x8010"])
assert.ok(
procedures.normalized_result.some(
(procedure) => procedure.address === address,
),
assert.equal(lowImage.normalized_result.base_address, lowEntry);
assert.equal(lowImage.normalized_result.file_bytes, lowBytes.length);
const fileMappings =
lowImage.normalized_result.observations.mappings.filter(
(mapping) => mapping.source_file_index !== null,
);
const instruction = evidence(
await call("inspect_native_instruction", { address: "0x8003" }),
sha256,
assert.equal(fileMappings.length, 1);
assert.equal(
fileMappings[0].address_space,
lowImage.normalized_result.observations.default_address_space,
);
assert.equal(instruction.normalized_result.mnemonic.toUpperCase(), "JSR");
assert.equal(fileMappings[0].overlay, false);
assert.equal(fileMappings[0].start, lowEntry);
assert.equal(fileMappings[0].length, lowBytes.length);
const lowRead = evidence(
await call("read_bytes", { address: lowEntry, length: linked.length }),
lowSha256,
);
assert.equal(lowRead.normalized_result.bytes_hex, linked.toString("hex"));
const lowCall = evidence(
await call("inspect_native_instruction", {
address: `0x${(entry + 2).toString(16)}`,
}),
lowSha256,
);
assert.equal(lowCall.normalized_result.mnemonic.toUpperCase(), "JSR");
assert.ok(
instruction.normalized_result.references.some(
(reference) => reference.call && reference.target_address === "0x8010",
lowCall.normalized_result.references.some(
(reference) =>
reference.call &&
reference.target_address === `0x${(entry + 15).toString(16)}`,
),
);
const pseudo = evidence(
await call("procedure_pseudo_code", { procedure: "0x8010" }),
sha256,
);
assert.match(pseudo.normalized_result, /\+\s*(?:3|0x3|'\\x03')/u);
const filePseudo = evidence(
await call("procedure_pseudo_code", { procedure: "file-offset:0xf" }),
sha256,
);
assert.equal(filePseudo.normalized_result, pseudo.normalized_result);
assert.ok(
filePseudo.locations.some(
(location) => location.kind === "file-offset" && location.offset === 15,
),
);
const rejected = await client.callTool({
name: "open_binary",
arguments: {
path,
format: "raw-binary",
architecture: "6502",
entry_point: "0xffff",
},
});
assert.equal(rejected.isError, true);
assert.match(JSON.stringify(rejected.content), /6502 address width/u);
const preserved = await call("binary_session");
assert.equal(preserved.analysis_run.run_id, status.analysis_run.run_id);
assert.equal(preserved.entry_point, "0x8001");
await call("close_binary");
await client.ping();
const { stdout } = await exec(
const lowCli = await exec(
process.execPath,
[
entrypoint,
"inspect-native-load-image",
path,
lowPath,
"--target-format",
"raw-binary",
"--architecture",
"6502",
"--entry-point",
"0x8001",
lowEntry,
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
);
const cli = parseEvidence(JSON.parse(stdout));
assert.deepEqual(cli.normalized_result, image.normalized_result);
assert.equal(cli.analysis_profile.digest, image.analysis_profile.digest);
assert.equal(cli.subject.digest.sha256, sha256);
const fileCli = await exec(
process.execPath,
[
entrypoint,
"decompile",
path,
"--file-offset=0xf",
"--target-format",
"raw-binary",
"--architecture",
"6502",
"--entry-point",
"0x8001",
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
const lowCliEvidence = evidence(JSON.parse(lowCli.stdout), lowSha256);
assert.deepEqual(
lowCliEvidence.normalized_result,
lowImage.normalized_result,
);
const fileCliEvidence = parseEvidence(JSON.parse(fileCli.stdout));
assert.equal(
fileCliEvidence.normalized_result,
filePseudo.normalized_result,
lowCliEvidence.analysis_profile.digest,
lowImage.analysis_profile.digest,
);
assert.deepEqual(fileCliEvidence.locations, filePseudo.locations);
assert.equal(fileCliEvidence.subject.digest.sha256, sha256);
assert.deepEqual(await readFile(path), bytes);
assert.deepEqual(await readFile(lowPath), lowBytes);
assert.deepEqual(await readdir(runtime), []);
const lowMemory = [];
// BinaryLoader maps file bytes before trying processor-defined blocks. Prove
// that overlapping zero-page/stack definitions never overlay the selected image.
for (const [entry, fullAddressSpace] of [
[0xff, false],
[0, true],
]) {
const lowPath = join(workspace, `low-${entry}.bin`);
await exec(
ld65,
[
"-C",
join(root, "tests/conformance/ghidra/6502.cfg"),
"--start-addr",
String(entry),
"-o",
lowPath,
object,
],
{ timeout: 10000 },
);
const linked = await readFile(lowPath);
const expectedCode = Buffer.from(
"a907200000c90ad003a92a60a9006018690360",
"hex",
);
expectedCode.writeUInt16LE(entry + 15, 3);
assert.deepEqual(linked, expectedCode);
const lowBytes = fullAddressSpace
? Buffer.concat([linked, Buffer.alloc(65536 - linked.length)])
: linked;
await writeFile(lowPath, lowBytes);
const lowSha256 = createHash("sha256").update(lowBytes).digest("hex");
const lowEntry = `0x${entry.toString(16)}`;
const lowOpened = await call("open_binary", {
path: lowPath,
lowMemory.push({
entry: lowEntry,
bytes: lowBytes.length,
sha256: lowSha256,
});
}
let windowsReparseAdmission = "not-applicable";
if (process.platform === "win32") {
const junction = join(workspace, "linked");
await symlink(workspace, junction, "junction");
try {
const linkedPath = join(junction, "image.bin");
// Reopening the same bytes through a junction must replace the direct session.
await call("open_binary", {
path,
format: "raw-binary",
architecture: "6502",
entry_point: lowEntry,
entry_point: "0x8001",
provider_id: "ghidra",
});
assert.equal(lowOpened.entry_point, lowEntry);
const lowImage = evidence(
const directImage = evidence(
await call("inspect_native_load_image"),
lowSha256,
sha256,
);
assert.equal(
lowImage.normalized_result.status,
"verified",
JSON.stringify(lowImage.normalized_result),
assert.equal(directImage.normalized_result.status, "verified");
// Canonical file identity must not bypass admission of the new path.
await call("open_binary", {
path: linkedPath,
format: "raw-binary",
architecture: "6502",
entry_point: "0x8001",
provider_id: "ghidra",
});
// open_binary selects a lazy session; the first analysis admits the snapshot.
const linkedImage = await client.callTool(
{ name: "inspect_native_load_image", arguments: {} },
{ timeout: 240000 },
);
assert.equal(lowImage.normalized_result.base_address, lowEntry);
assert.equal(lowImage.normalized_result.file_bytes, lowBytes.length);
const fileMappings =
lowImage.normalized_result.observations.mappings.filter(
(mapping) => mapping.source_file_index !== null,
);
assert.equal(fileMappings.length, 1);
assert.equal(
fileMappings[0].address_space,
lowImage.normalized_result.observations.default_address_space,
);
assert.equal(fileMappings[0].overlay, false);
assert.equal(fileMappings[0].start, lowEntry);
assert.equal(fileMappings[0].length, lowBytes.length);
const lowRead = evidence(
await call("read_bytes", { address: lowEntry, length: linked.length }),
lowSha256,
);
assert.equal(lowRead.normalized_result.bytes_hex, linked.toString("hex"));
const lowCall = evidence(
await call("inspect_native_instruction", {
address: `0x${(entry + 2).toString(16)}`,
}),
lowSha256,
);
assert.equal(lowCall.normalized_result.mnemonic.toUpperCase(), "JSR");
assert.ok(
lowCall.normalized_result.references.some(
(reference) =>
reference.call &&
reference.target_address === `0x${(entry + 15).toString(16)}`,
),
assert.equal(linkedImage.isError, true);
assert.match(
JSON.stringify(linkedImage),
/Reparse path component is unsupported/u,
);
await call("close_binary");
const lowCli = await exec(
process.execPath,
[
entrypoint,
"inspect-native-load-image",
lowPath,
"--target-format",
"raw-binary",
"--architecture",
"6502",
"--entry-point",
lowEntry,
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
);
const lowCliEvidence = evidence(JSON.parse(lowCli.stdout), lowSha256);
assert.deepEqual(
lowCliEvidence.normalized_result,
lowImage.normalized_result,
);
assert.equal(
lowCliEvidence.analysis_profile.digest,
lowImage.analysis_profile.digest,
);
assert.deepEqual(await readFile(lowPath), lowBytes);
assert.deepEqual(await readdir(runtime), []);
lowMemory.push({
entry: lowEntry,
bytes: lowBytes.length,
sha256: lowSha256,
const snapshotPath = join(workspace, "direct-snapshot.json");
const cliArgs = (selectedPath) => [
entrypoint,
"analyze",
selectedPath,
"--target-format",
"raw-binary",
"--architecture",
"6502",
"--entry-point",
"0x8001",
"--provider",
"ghidra",
"--snapshot",
snapshotPath,
"--json",
];
const saved = await exec(process.execPath, cliArgs(path), {
env,
timeout: 240000,
maxBuffer: 16 * 1024 * 1024,
});
}
let windowsReparseAdmission = "not-applicable";
if (process.platform === "win32") {
const junction = join(workspace, "linked");
await symlink(workspace, junction, "junction");
try {
const linkedPath = join(junction, "image.bin");
// Reopening the same bytes through a junction must replace the direct session.
await call("open_binary", {
path,
format: "raw-binary",
architecture: "6502",
entry_point: "0x8001",
provider_id: "ghidra",
});
const directImage = evidence(
await call("inspect_native_load_image"),
sha256,
);
assert.equal(directImage.normalized_result.status, "verified");
// Canonical file identity must not bypass admission of the new path.
await call("open_binary", {
path: linkedPath,
format: "raw-binary",
architecture: "6502",
entry_point: "0x8001",
provider_id: "ghidra",
});
// open_binary selects a lazy session; the first analysis admits the snapshot.
const linkedImage = await client.callTool(
{ name: "inspect_native_load_image", arguments: {} },
{ timeout: 240000 },
);
assert.equal(linkedImage.isError, true);
assert.match(
JSON.stringify(linkedImage),
/Reparse path component is unsupported/u,
);
await call("close_binary");
const snapshotPath = join(workspace, "direct-snapshot.json");
const cliArgs = (selectedPath) => [
entrypoint,
"analyze",
selectedPath,
"--target-format",
"raw-binary",
"--architecture",
"6502",
"--entry-point",
"0x8001",
"--provider",
"ghidra",
"--snapshot",
snapshotPath,
"--json",
];
const saved = await exec(process.execPath, cliArgs(path), {
assert.equal(
evidence(JSON.parse(saved.stdout), sha256).operation,
"binary_overview",
);
const savedSnapshot = JSON.parse(await readFile(snapshotPath, "utf8"));
assert.ok(
savedSnapshot.workflow_entries.some(
({ operation }) => operation === "binary_overview",
),
);
assert.deepEqual(await readdir(runtime), []);
// Replaying a matching snapshot must still reach native path admission.
await assert.rejects(
exec(process.execPath, cliArgs(linkedPath), {
env,
timeout: 240000,
maxBuffer: 16 * 1024 * 1024,
});
assert.equal(
evidence(JSON.parse(saved.stdout), sha256).operation,
"binary_overview",
);
const savedSnapshot = JSON.parse(await readFile(snapshotPath, "utf8"));
assert.ok(
savedSnapshot.workflow_entries.some(
({ operation }) => operation === "binary_overview",
),
);
assert.deepEqual(await readdir(runtime), []);
// Replaying a matching snapshot must still reach native path admission.
await assert.rejects(
exec(process.execPath, cliArgs(linkedPath), {
env,
timeout: 240000,
maxBuffer: 16 * 1024 * 1024,
}),
(cause) => {
assert.match(
cause.stdout + cause.stderr,
/Reparse path component is unsupported/u,
);
return true;
},
);
assert.deepEqual(await readFile(path), bytes);
assert.deepEqual(await readdir(runtime), []);
windowsReparseAdmission = "cli-snapshot-mcp-rejected";
} finally {
await unlink(junction);
}
}),
(cause) => {
assert.match(
cause.stdout + cause.stderr,
/Reparse path component is unsupported/u,
);
return true;
},
);
assert.deepEqual(await readFile(path), bytes);
assert.deepEqual(await readdir(runtime), []);
windowsReparseAdmission = "cli-snapshot-mcp-rejected";
} finally {
await unlink(junction);
}
report = {
ok: true,
mocked: false,
lane: "ghidra-6502",
host: process.platform,
toolchain,
provider: image.provider,
target: {
sha256,
bytes: bytes.length,
architecture: "6502",
entry: "0x8001",
},
cli_mcp_load_image_parity: true,
instructions_and_call_reference: true,
decompilation: true,
file_offset_decompilation_cli_mcp: true,
invalid_mapping_preserves_session: true,
low_memory_cli_mcp_parity: lowMemory,
windows_reparse_admission: windowsReparseAdmission,
source_unchanged: true,
runtime_cleanup: "complete",
};
} catch (cause) {
if (stderr) process.stderr.write(stderr);
throw cause;
}
}, [
async () => {
await closeVerifierResources([
async () => {
if (connected) await call("close_binary", {});
},
() => client.close(),
() => transport.close(),
]);
assert.deepEqual(
(await readdir(runtime)).filter((name) => name.startsWith("rea-ghidra-")),
[],
"Owned Ghidra runtime survived close",
);
verifierRun = await completeVerifierRun(run);
if (process.platform !== "win32") {
assert.equal(verifierRun.process_lineage.status, "verified");
assert.deepEqual(verifierRun.process_lineage.descendants, []);
}
await rm(workspace, { recursive: true, force: true });
},
]);
verifierRun = await completeVerifierRun(run);
report = {
ok: true,
mocked: false,
lane: "ghidra-6502",
host: process.platform,
toolchain,
provider: image.provider,
target: {
sha256,
bytes: bytes.length,
architecture: "6502",
entry: "0x8001",
},
cli_mcp_load_image_parity: true,
instructions_and_call_reference: true,
decompilation: true,
file_offset_decompilation_cli_mcp: true,
invalid_mapping_preserves_session: true,
low_memory_cli_mcp_parity: lowMemory,
windows_reparse_admission: windowsReparseAdmission,
source_unchanged: true,
runtime_cleanup: "complete",
};
} catch (cause) {
if (stderr) process.stderr.write(stderr);
throw cause;
} finally {
try {
await client.close();
} finally {
await transport.close();
}
}
const verifierRun = await completeVerifierRun(run);
if (process.platform !== "win32") {
assert.equal(verifierRun.process_lineage.status, "verified");
assert.deepEqual(verifierRun.process_lineage.descendants, []);
}
// Keep the workspace for diagnosis unless verification and cleanup both pass.
await rm(workspace, { recursive: true, force: true });
process.stdout.write(
JSON.stringify({ verifier_run: verifierRun, ...report }) + "\n",
);
+21 -14
View File
@@ -1,6 +1,4 @@
#!/usr/bin/env node
import { createVerifierCommands } from "./lib/verify-commands.mjs";
import { createVerifierMcpOwner } from "./lib/verifier-mcp-owner.mjs";
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
@@ -63,7 +61,6 @@ for (const name of [
"REA_GHIDRA_DISABLED_ANALYZERS",
])
delete env[name];
const compilerCommands = await createVerifierCommands();
const report = {
verifier: "ghidra-owned-checkpoints",
phases: [],
@@ -122,7 +119,6 @@ async function refuseCli(procedure, flags, expected) {
return refusal;
}
async function connect(environment = {}) {
const owner = createVerifierMcpOwner();
const client = new Client({
name: "rea-ghidra-checkpoint-verifier",
version: "1",
@@ -134,15 +130,23 @@ async function connect(environment = {}) {
stderr: "pipe",
});
transport.stderr?.on("data", () => {});
compilerCommands.adopt(owner, "checkpoint MCP connection");
await owner.open(client, transport);
try {
await client.connect(transport);
} catch (cause) {
await client.close();
await transport.close();
throw cause;
}
const raw = (name, args = {}) =>
client.callTool({ name, arguments: args }, { timeout: 240000 });
return {
raw,
call: async (name, args = {}) =>
requireMcpOperationResult(await raw(name, args), name),
close: () => owner.close(),
close: async () => {
await client.close();
await transport.close();
},
};
}
let manifestPath;
@@ -156,11 +160,15 @@ __attribute__((noinline)) int rea_checkpoint_leaf(CheckpointRecord *record) { re
int main(void) { CheckpointRecord record = { 3, 11 }; return rea_checkpoint_leaf(&record); }
`,
);
await compilerCommands.exec(
"cc",
["-O0", "-g", "-gdwarf-4", "-fno-inline", sourcePath, "-o", target],
{ timeout: 120_000, maxBuffer: 16 * 1024 * 1024 },
);
await exec("cc", [
"-O0",
"-g",
"-gdwarf-4",
"-fno-inline",
sourcePath,
"-o",
target,
]);
const targetDigest = await sha(target);
report.target = {
sha256: targetDigest,
@@ -458,10 +466,9 @@ int main(void) { CheckpointRecord record = { 3, 11 }; return rea_checkpoint_leaf
report.failure_stderr = cause.stderr.slice(-4000);
process.exitCode = 1;
} finally {
report.run = await completeVerifierRun(run);
let cleanupVerified = false;
try {
await compilerCommands.close();
report.run = await completeVerifierRun(run);
assertVerifierCleanupComplete(report.run.process_lineage);
cleanupVerified = true;
} catch (cause) {
+363 -392
View File
@@ -1,8 +1,4 @@
#!/usr/bin/env node
import {
closeVerifierResources,
withVerifierCleanup,
} from "./lib/verifier-cleanup.mjs";
import assert from "node:assert/strict";
import {
requireMcpLifecycleResult,
@@ -11,14 +7,7 @@ import {
} from "./lib/mcp-verifier-results.mjs";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import {
mkdtemp,
mkdir,
readFile,
readdir,
rm,
writeFile,
} from "node:fs/promises";
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { isAbsolute, join } from "node:path";
import { fileURLToPath } from "node:url";
@@ -99,404 +88,386 @@ const transport = new StdioClientTransport({
stderr: "pipe",
});
const client = new Client({ name: "rea-real-com-proof", version: "1" });
let opened = false;
let stderr = "";
let connected = false;
let verifierRun;
let report;
transport.stderr?.on("data", (chunk) => {
stderr = (stderr + chunk.toString()).slice(-65536);
});
await withVerifierCleanup(async () => {
try {
await client.connect(transport);
connected = true;
try {
await client.connect(transport);
const untyped = await client.callTool({
name: "open_binary",
arguments: { path: targetPath, provider_id: "ghidra" },
});
assert.equal(
untyped.isError,
true,
"Headerless bytes were guessed as COM without explicit interpretation",
const untyped = await client.callTool({
name: "open_binary",
arguments: { path: targetPath, provider_id: "ghidra" },
});
assert.equal(
untyped.isError,
true,
"Headerless bytes were guessed as COM without explicit interpretation",
);
const target = await call("open_binary", {
path: targetPath,
provider_id: "ghidra",
format: "dos-com",
});
opened = true;
assert.equal(target.format, "dos-com");
assert.equal(target.architecture, "x86");
assert.equal(target.sha256, sha256);
const image = await call("inspect_native_load_image");
assert.equal(
image.status,
"verified",
JSON.stringify(image.checks?.filter((c) => !c.matched)),
);
assert.equal(image.format, "dos-com");
assert.equal(image.header_bytes, 0);
assert.equal(image.module_bytes, fixture.bytes.length);
assert.equal(image.entry.linear_address, fixture.entry);
assert.equal(image.entry.offset, 256);
assert.equal(image.observations.entry_context[0].registers.length, 4);
const memory = await call("read_bytes", {
address: fixture.entry,
length: fixture.bytes.length,
});
assert.equal(memory.bytes_hex, fixture.bytes.toString("hex"));
assert.equal(memory.complete, true);
const mapping = await call("address_to_file_offset", {
address: fixture.near,
});
assert.equal(mapping.file_offset, 32);
const psp = await call("read_bytes", { address: "0x10000", length: 16 });
assert.equal(psp.returned_bytes, 0);
assert.equal(psp.complete, false);
const tail = await call("read_bytes", { address: "0x1013e", length: 8 });
assert.equal(tail.returned_bytes, 2);
assert.equal(tail.complete, false);
const procedures = await call("list_procedures");
assert.ok(procedures.some((p) => p.address === fixture.entry));
assert.ok(procedures.some((p) => p.address === fixture.near));
const dossier = functionDossierSchema.parse(
await call("analyze_function", {
procedure: fixture.entry,
detail: "complete",
}),
);
assert.ok(dossier.pseudocode.trim().length > 0);
assert.equal(dossier.procedure.body.available, true);
const instruction = await call("inspect_native_instruction", {
address: fixture.entry,
});
assert.equal(
instruction.bytes.replaceAll(" ", "").toLowerCase(),
fixture.entry_hex,
);
// This NOP follows the entry's terminating INT 20h and is not reached by
// either function. It remains valid instruction bytes outside the listing.
const unlistedAddress = "0x1013f";
const unlistedBytes = await call("read_bytes", {
address: unlistedAddress,
length: 1,
});
assert.equal(unlistedBytes.bytes_hex, "90");
const unlistedResponse = await client.callTool({
name: "inspect_native_instruction",
arguments: { address: unlistedAddress },
});
const unlistedEvidence = parseEvidence(
requireMcpOutput(unlistedResponse, "inspect_native_instruction"),
);
const unlisted = unlistedEvidence.normalized_result;
assert.equal(unlisted.status, "not-in-listing");
assert.equal(unlisted.bytes, null);
assert.ok(
unlisted.limitations.some((item) =>
item.includes("did not attempt a decode"),
),
);
const tools = (await client.listTools()).tools;
const advertised = tools.find(
(tool) => tool.name === "inspect_native_instruction",
);
assert.ok(advertised?.outputSchema);
const ajv = new Ajv2020({ strict: false, validateFormats: false });
assert.equal(ajv.validateSchema(advertised.outputSchema), true);
assert.equal(
ajv.validate(advertised.outputSchema, unlistedResponse.structuredContent),
true,
JSON.stringify(ajv.errors),
);
assert.ok(
nativeInstructionSchema.safeParse({ ...unlisted, status: "undecodable" })
.success,
);
const contextDefinition = tools.find(
(tool) => tool.name === "inspect_address_context",
);
assert.ok(contextDefinition?.outputSchema);
const documents = await call("list_documents");
assert.equal(documents.length, 1);
const document = documents[0];
for (const input of [
{ address: fixture.entry, document: `${document}-not-a-program` },
{ address: "not-an-address", document },
]) {
const direct = requireMcpToolError(
await client.callTool({ name: "address_name", arguments: input }),
);
const target = await call("open_binary", {
path: targetPath,
provider_id: "ghidra",
format: "dos-com",
});
assert.equal(target.format, "dos-com");
assert.equal(target.architecture, "x86");
assert.equal(target.sha256, sha256);
const image = await call("inspect_native_load_image");
assert.equal(
image.status,
"verified",
JSON.stringify(image.checks?.filter((c) => !c.matched)),
const response = await client.callTool(
{ name: "inspect_address_context", arguments: input },
{ timeout: 240000, toolDefinition: contextDefinition },
);
assert.equal(image.format, "dos-com");
assert.equal(image.header_bytes, 0);
assert.equal(image.module_bytes, fixture.bytes.length);
assert.equal(image.entry.linear_address, fixture.entry);
assert.equal(image.entry.offset, 256);
assert.equal(image.observations.entry_context[0].registers.length, 4);
const memory = await call("read_bytes", {
const context = requireMcpLifecycleResult(
response,
"inspect_address_context",
);
assert.equal(context.name.state, "unavailable");
assert.equal(context.name.error.code, "invalid_request");
assert.deepEqual(context.name.error.details.issues, direct.details.issues);
assert.equal(context.name.reason, direct.message);
assert.equal(context.name.remediation, direct.remediation.action);
}
assert.equal(
(await call("resolve_native_call_targets", { address: unlistedAddress }))
.status,
"unavailable",
);
assert.deepEqual(
(await cli("inspect-native-instruction", unlistedAddress))
.normalized_result,
unlisted,
);
const cliImage = await cli("inspect-native-load-image");
const stable = (x) => ({
...x,
observations: {
...x.observations,
source_files: x.observations.source_files.map(
({ name: _name, ...file }) => file,
),
},
});
assert.deepEqual(stable(cliImage.normalized_result), stable(image));
assert.deepEqual(
(
await cli("read-bytes", fixture.entry, [
"--length",
String(fixture.bytes.length),
])
).normalized_result,
memory,
);
assert.deepEqual(
(await cli("address-to-file-offset", fixture.near)).normalized_result,
mapping,
);
const changes = {
procedure: fixture.entry,
name: "rea_entry",
comment: "Regular\nUnicode 註記",
inline_comment: "Inline finding",
};
// Prime provider inventories and the session snapshot before edits.
await call("list_names");
const updated = await call("annotate_native_function", changes);
assert.equal(updated.annotations.name, changes.name);
assert.equal(updated.annotations.comment, changes.comment);
assert.equal(updated.annotations.inline_comment, changes.inline_comment);
assert.equal(updated.dossier.procedure.name, changes.name);
assert.ok(updated.dossier.pseudocode.includes(changes.name));
// Exercise the documented recovery through the public MCP catalog on real
// Ghidra, rather than assuming the provider exposes direct comment methods.
const contextCall = async (name, args) => {
const definition = tools.find((tool) => tool.name === name);
assert.ok(definition?.outputSchema, `${name} must be advertised`);
const response = await client.callTool(
{ name, arguments: args },
{ timeout: 240000, toolDefinition: definition },
);
return requireMcpOutput(response, name);
};
const context = (
await contextCall("inspect_address_context", {
address: fixture.entry,
length: fixture.bytes.length,
});
assert.equal(memory.bytes_hex, fixture.bytes.toString("hex"));
assert.equal(memory.complete, true);
const mapping = await call("address_to_file_offset", {
address: fixture.near,
});
assert.equal(mapping.file_offset, 32);
const psp = await call("read_bytes", { address: "0x10000", length: 16 });
assert.equal(psp.returned_bytes, 0);
assert.equal(psp.complete, false);
const tail = await call("read_bytes", { address: "0x1013e", length: 8 });
assert.equal(tail.returned_bytes, 2);
assert.equal(tail.complete, false);
const procedures = await call("list_procedures");
assert.ok(procedures.some((p) => p.address === fixture.entry));
assert.ok(procedures.some((p) => p.address === fixture.near));
const dossier = functionDossierSchema.parse(
})
).result;
assert.equal(context.procedure.state, "available");
assert.equal(context.procedure.value.found, true);
for (const facet of [context.comment, context.inline_comment]) {
assert.equal(facet.state, "unavailable");
assert.match(
facet.remediation,
/analyze_function.*parent_evidence_id.*inspect_analysis_view/u,
);
}
assert.match(context.bookmarks.remediation, /binary_session/u);
const summary = parseEvidence(
await contextCall("analyze_function", {
procedure: context.procedure.value.procedure.address,
}),
).normalized_result;
const comments = parseEvidence(
await contextCall("inspect_analysis_view", {
source: {
kind: "retained-evidence",
evidence_id: summary.parent_evidence_id,
},
view: { kind: "native", facet: "comments" },
}),
).normalized_result;
for (const [kind, text] of [
["comment", changes.comment],
["inline", changes.inline_comment],
])
assert.ok(
comments.item.some(
(item) =>
item.address === context.procedure.value.query_address &&
item.kind === kind &&
item.text === text,
),
);
const outside = (
await contextCall("inspect_address_context", {
address: unlistedAddress,
})
).result;
assert.equal(outside.procedure.state, "available");
assert.equal(outside.procedure.value.found, false);
assert.equal(outside.comment.state, "unavailable");
assert.match(outside.comment.remediation, /binary_session/u);
assert.equal(
await call("procedure_address", { procedure: changes.name }),
fixture.entry,
);
assert.equal(
await call("address_name", { address: fixture.entry }),
changes.name,
);
assert.ok(
(await call("list_procedures")).some((p) => p.value === changes.name),
);
assert.ok((await call("list_names")).some((p) => p.value === changes.name));
assert.equal(
(
await call("analyze_function", {
procedure: fixture.entry,
detail: "complete",
}),
);
assert.ok(dossier.pseudocode.trim().length > 0);
assert.equal(dossier.procedure.body.available, true);
const instruction = await call("inspect_native_instruction", {
address: fixture.entry,
});
assert.equal(
instruction.bytes.replaceAll(" ", "").toLowerCase(),
fixture.entry_hex,
);
// This NOP follows the entry's terminating INT 20h and is not reached by
// either function. It remains valid instruction bytes outside the listing.
const unlistedAddress = "0x1013f";
const unlistedBytes = await call("read_bytes", {
address: unlistedAddress,
length: 1,
});
assert.equal(unlistedBytes.bytes_hex, "90");
const unlistedResponse = await client.callTool({
name: "inspect_native_instruction",
arguments: { address: unlistedAddress },
});
const unlistedEvidence = parseEvidence(
requireMcpOutput(unlistedResponse, "inspect_native_instruction"),
);
const unlisted = unlistedEvidence.normalized_result;
assert.equal(unlisted.status, "not-in-listing");
assert.equal(unlisted.bytes, null);
assert.ok(
unlisted.limitations.some((item) =>
item.includes("did not attempt a decode"),
),
);
const tools = (await client.listTools()).tools;
const advertised = tools.find(
(tool) => tool.name === "inspect_native_instruction",
);
assert.ok(advertised?.outputSchema);
const ajv = new Ajv2020({ strict: false, validateFormats: false });
assert.equal(ajv.validateSchema(advertised.outputSchema), true);
assert.equal(
ajv.validate(advertised.outputSchema, unlistedResponse.structuredContent),
true,
JSON.stringify(ajv.errors),
);
assert.ok(
nativeInstructionSchema.safeParse({ ...unlisted, status: "undecodable" })
.success,
);
const contextDefinition = tools.find(
(tool) => tool.name === "inspect_address_context",
);
assert.ok(contextDefinition?.outputSchema);
const documents = await call("list_documents");
assert.equal(documents.length, 1);
const document = documents[0];
for (const input of [
{ address: fixture.entry, document: `${document}-not-a-program` },
{ address: "not-an-address", document },
]) {
const direct = requireMcpToolError(
await client.callTool({ name: "address_name", arguments: input }),
);
const response = await client.callTool(
{ name: "inspect_address_context", arguments: input },
{ timeout: 240000, toolDefinition: contextDefinition },
);
const context = requireMcpLifecycleResult(
response,
"inspect_address_context",
);
assert.equal(context.name.state, "unavailable");
assert.equal(context.name.error.code, "invalid_request");
assert.deepEqual(
context.name.error.details.issues,
direct.details.issues,
);
assert.equal(context.name.reason, direct.message);
assert.equal(context.name.remediation, direct.remediation.action);
}
assert.equal(
(await call("resolve_native_call_targets", { address: unlistedAddress }))
.status,
"unavailable",
);
assert.deepEqual(
(await cli("inspect-native-instruction", unlistedAddress))
.normalized_result,
unlisted,
);
const cliImage = await cli("inspect-native-load-image");
const stable = (x) => ({
...x,
observations: {
...x.observations,
source_files: x.observations.source_files.map(
({ name: _name, ...file }) => file,
),
},
});
assert.deepEqual(stable(cliImage.normalized_result), stable(image));
assert.deepEqual(
(
await cli("read-bytes", fixture.entry, [
"--length",
String(fixture.bytes.length),
])
).normalized_result,
memory,
);
assert.deepEqual(
(await cli("address-to-file-offset", fixture.near)).normalized_result,
mapping,
);
const changes = {
})
).procedure.name,
changes.name,
);
const rejected = await client.callTool({
name: "annotate_native_function",
arguments: {
procedure: fixture.entry,
name: "rea_entry",
comment: "Regular\nUnicode 註記",
inline_comment: "Inline finding",
};
// Prime provider inventories and the session snapshot before edits.
await call("list_names");
const updated = await call("annotate_native_function", changes);
assert.equal(updated.annotations.name, changes.name);
assert.equal(updated.annotations.comment, changes.comment);
assert.equal(updated.annotations.inline_comment, changes.inline_comment);
assert.equal(updated.dossier.procedure.name, changes.name);
assert.ok(updated.dossier.pseudocode.includes(changes.name));
// Exercise the documented recovery through the public MCP catalog on real
// Ghidra, rather than assuming the provider exposes direct comment methods.
const contextCall = async (name, args) => {
const definition = tools.find((tool) => tool.name === name);
assert.ok(definition?.outputSchema, `${name} must be advertised`);
const response = await client.callTool(
{ name, arguments: args },
{ timeout: 240000, toolDefinition: definition },
);
return requireMcpOutput(response, name);
};
const context = (
await contextCall("inspect_address_context", {
comment: "MUST ROLL BACK",
name: "invalid name\n",
},
});
assert.equal(
rejected.isError,
true,
"Ghidra accepted an invalid function name",
);
const retained = await call("annotate_native_function", {
procedure: fixture.entry,
name: changes.name,
});
assert.equal(
retained.annotations.comment,
changes.comment,
"Failed edit leaked its earlier comment write",
);
assert.equal(retained.annotations.inline_comment, changes.inline_comment);
assert.equal(retained.dossier.procedure.name, changes.name);
await verifyGhidraAddressBoundaries(client, fixture.entry, cli);
const cleared = await call("annotate_native_function", {
procedure: fixture.entry,
comment: "",
inline_comment: "",
});
assert.equal(cleared.annotations.comment, null);
assert.equal(cleared.annotations.inline_comment, null);
assert.ok(!cleared.dossier.comments.some((c) => c.address === fixture.entry));
assert.equal(
(
await call("read_bytes", {
address: fixture.entry,
length: fixture.bytes.length,
})
).result;
assert.equal(context.procedure.state, "available");
assert.equal(context.procedure.value.found, true);
for (const facet of [context.comment, context.inline_comment]) {
assert.equal(facet.state, "unavailable");
assert.match(
facet.remediation,
/analyze_function.*parent_evidence_id.*inspect_analysis_view/u,
);
}
assert.match(context.bookmarks.remediation, /binary_session/u);
const summary = parseEvidence(
await contextCall("analyze_function", {
procedure: context.procedure.value.procedure.address,
}),
).normalized_result;
const comments = parseEvidence(
await contextCall("inspect_analysis_view", {
source: {
kind: "retained-evidence",
evidence_id: summary.parent_evidence_id,
},
view: { kind: "native", facet: "comments" },
}),
).normalized_result;
for (const [kind, text] of [
["comment", changes.comment],
["inline", changes.inline_comment],
])
assert.ok(
comments.item.some(
(item) =>
item.address === context.procedure.value.query_address &&
item.kind === kind &&
item.text === text,
),
);
const outside = (
await contextCall("inspect_address_context", {
address: unlistedAddress,
})
).result;
assert.equal(outside.procedure.state, "available");
assert.equal(outside.procedure.value.found, false);
assert.equal(outside.comment.state, "unavailable");
assert.match(outside.comment.remediation, /binary_session/u);
assert.equal(
await call("procedure_address", { procedure: changes.name }),
fixture.entry,
).bytes_hex,
fixture.bytes.toString("hex"),
);
assert.equal((await call("inspect_native_load_image")).status, "verified");
const cliAnnotated = await cli("annotate-native-function", fixture.entry, [
"--name",
changes.name,
"--comment",
changes.comment,
"--inline-comment",
changes.inline_comment,
]);
assert.deepEqual(cliAnnotated.normalized_result, updated);
if (captureDirectory !== undefined)
await writeFile(
join(captureDirectory, "image.json"),
JSON.stringify(image, null, 2),
{ mode: 0o600 },
);
assert.equal(
await call("address_name", { address: fixture.entry }),
changes.name,
await call("close_binary");
opened = false;
assert.equal(digest(await readFile(targetPath)), sha256);
report = {
ok: true,
mocked: false,
lane: "ghidra-com",
packaged_artifact: packageRoot !== undefined,
host: process.platform,
provider: { id: "ghidra", version: installation.providerVersion },
target: { format: "dos-com", sha256, bytes: fixture.bytes.length },
load_image: {
status: image.status,
checks: image.checks.length,
entry: fixture.entry,
entry_context: image.observations.entry_context,
},
transport: { cli: true, stdio_mcp: true },
memory_evidence: {
complete_file_read: true,
source_offset: 32,
psp_unmapped: true,
partial_read_bytes: 2,
},
functions: { entry: true, near: true, pseudocode_nonempty: true },
annotations: {
atomic_rollback: true,
fresh_inventories: true,
fresh_decompiler: true,
clear_comments: true,
cli_mcp_parity: true,
address_context_comment_recovery: true,
},
context_input_errors_preserved: true,
original_unchanged: true,
address_truncation_rejected: true,
limitations: image.limitations,
};
} catch (error) {
if (stderr.trim()) process.stderr.write(stderr);
throw error;
} finally {
if (opened)
await call("close_binary").catch((error) =>
process.stderr.write(String(error)),
);
assert.ok(
(await call("list_procedures")).some((p) => p.value === changes.name),
);
assert.ok((await call("list_names")).some((p) => p.value === changes.name));
assert.equal(
(
await call("analyze_function", {
procedure: fixture.entry,
detail: "complete",
})
).procedure.name,
changes.name,
);
const rejected = await client.callTool({
name: "annotate_native_function",
arguments: {
procedure: fixture.entry,
comment: "MUST ROLL BACK",
name: "invalid name\n",
},
});
assert.equal(
rejected.isError,
true,
"Ghidra accepted an invalid function name",
);
const retained = await call("annotate_native_function", {
procedure: fixture.entry,
name: changes.name,
});
assert.equal(
retained.annotations.comment,
changes.comment,
"Failed edit leaked its earlier comment write",
);
assert.equal(retained.annotations.inline_comment, changes.inline_comment);
assert.equal(retained.dossier.procedure.name, changes.name);
await verifyGhidraAddressBoundaries(client, fixture.entry, cli);
const cleared = await call("annotate_native_function", {
procedure: fixture.entry,
comment: "",
inline_comment: "",
});
assert.equal(cleared.annotations.comment, null);
assert.equal(cleared.annotations.inline_comment, null);
assert.ok(
!cleared.dossier.comments.some((c) => c.address === fixture.entry),
);
assert.equal(
(
await call("read_bytes", {
address: fixture.entry,
length: fixture.bytes.length,
})
).bytes_hex,
fixture.bytes.toString("hex"),
);
assert.equal((await call("inspect_native_load_image")).status, "verified");
const cliAnnotated = await cli("annotate-native-function", fixture.entry, [
"--name",
changes.name,
"--comment",
changes.comment,
"--inline-comment",
changes.inline_comment,
]);
assert.deepEqual(cliAnnotated.normalized_result, updated);
if (captureDirectory !== undefined)
await writeFile(
join(captureDirectory, "image.json"),
JSON.stringify(image, null, 2),
{ mode: 0o600 },
);
await call("close_binary");
assert.equal(digest(await readFile(targetPath)), sha256);
report = {
ok: true,
mocked: false,
lane: "ghidra-com",
packaged_artifact: packageRoot !== undefined,
host: process.platform,
provider: { id: "ghidra", version: installation.providerVersion },
target: { format: "dos-com", sha256, bytes: fixture.bytes.length },
load_image: {
status: image.status,
checks: image.checks.length,
entry: fixture.entry,
entry_context: image.observations.entry_context,
},
transport: { cli: true, stdio_mcp: true },
memory_evidence: {
complete_file_read: true,
source_offset: 32,
psp_unmapped: true,
partial_read_bytes: 2,
},
functions: { entry: true, near: true, pseudocode_nonempty: true },
annotations: {
atomic_rollback: true,
fresh_inventories: true,
fresh_decompiler: true,
clear_comments: true,
cli_mcp_parity: true,
address_context_comment_recovery: true,
},
context_input_errors_preserved: true,
original_unchanged: true,
address_truncation_rejected: true,
limitations: image.limitations,
};
} catch (error) {
if (stderr.trim()) process.stderr.write(stderr);
throw error;
}
}, [
async () => {
await closeVerifierResources([
async () => {
if (connected) await call("close_binary", {});
},
() => client.close(),
() => transport.close(),
]);
assert.deepEqual(
(await readdir(runtime)).filter((name) => name.startsWith("rea-ghidra-")),
[],
"Owned Ghidra runtime survived close",
);
verifierRun = await completeVerifierRun(run);
assert.equal(verifierRun.process_lineage.status, "verified");
assert.deepEqual(verifierRun.process_lineage.descendants, []);
await rm(workspace, { recursive: true, force: true });
},
]);
await client.close();
await transport.close();
await rm(workspace, { recursive: true, force: true });
}
assert.ok(report);
verifierRun = await completeVerifierRun(run);
const verifierRun = await completeVerifierRun(run);
assert.equal(verifierRun.process_lineage.status, "verified");
assert.deepEqual(verifierRun.process_lineage.descendants, []);
process.stdout.write(
+3 -6
View File
@@ -115,12 +115,9 @@ async function runPass({ fixture, languageOverride, label, layout }) {
stderr = (stderr + chunk.toString()).slice(-65536);
});
const call = async (name, args = {}) => {
const reply = await client.callTool(
{ name, arguments: args },
{
timeout: 180000,
},
);
const reply = await client.callTool({ name, arguments: args }, undefined, {
timeout: 180000,
});
return requireMcpOperationResult(reply, name);
};
const report = { label, language_override: languageOverride ?? null };
+399 -416
View File
@@ -1,8 +1,4 @@
#!/usr/bin/env node
import {
closeVerifierResources,
withVerifierCleanup,
} from "./lib/verifier-cleanup.mjs";
import assert from "node:assert/strict";
import {
requireMcpOperationResult,
@@ -103,440 +99,427 @@ const transport = new StdioClientTransport({
stderr: "pipe",
});
const client = new Client({ name: "rea-real-dos-proof", version: "1" });
let opened = false;
let stderr = "";
let connected = false;
let verifierRun;
let report;
await withVerifierCleanup(async () => {
try {
await client.connect(transport);
connected = true;
transport.stderr?.on("data", (chunk) => {
stderr = (stderr + chunk.toString()).slice(-65536);
});
const target = await call("open_binary", {
path: targetPath,
provider_id: "ghidra",
});
assert.equal(target.format, "dos-mz");
assert.equal(target.kind, "executable");
assert.equal(target.architecture, "x86");
assert.equal(target.sha256, sha256);
const image = await call("inspect_native_load_image");
assert.equal(
image.status,
"verified",
JSON.stringify(image.checks?.filter((check) => !check.matched)),
);
assert.equal(image.target_sha256, sha256);
assert.equal(image.header_bytes, fixture.header_bytes);
assert.equal(image.module_bytes, fixture.module_bytes);
assert.equal(image.entry.linear_address, expected.entry);
assert.ok(image.checks.every((check) => check.matched));
assert.ok(image.observations.relocations.length > 0);
assert.ok(
image.observations.relocations.every(
({ symbol }) => symbol === null || typeof symbol === "string",
try {
await client.connect(transport);
transport.stderr?.on("data", (chunk) => {
stderr = (stderr + chunk.toString()).slice(-65536);
});
const target = await call("open_binary", {
path: targetPath,
provider_id: "ghidra",
});
opened = true;
assert.equal(target.format, "dos-mz");
assert.equal(target.kind, "executable");
assert.equal(target.architecture, "x86");
assert.equal(target.sha256, sha256);
const image = await call("inspect_native_load_image");
assert.equal(
image.status,
"verified",
JSON.stringify(image.checks?.filter((check) => !check.matched)),
);
assert.equal(image.target_sha256, sha256);
assert.equal(image.header_bytes, fixture.header_bytes);
assert.equal(image.module_bytes, fixture.module_bytes);
assert.equal(image.entry.linear_address, expected.entry);
assert.ok(image.checks.every((check) => check.matched));
assert.ok(image.observations.relocations.length > 0);
assert.ok(
image.observations.relocations.every(
({ symbol }) => symbol === null || typeof symbol === "string",
),
"Each real Ghidra relocation must record its symbol observation",
);
const cliImage = await cliEvidence("inspect-native-load-image");
// Snapshot filenames are session-specific. Every measured byte/range/check is stable.
const stableImage = (value) => ({
...value,
observations: {
...value.observations,
source_files: value.observations.source_files.map(
({ name: _name, ...file }) => file,
),
"Each real Ghidra relocation must record its symbol observation",
);
const cliImage = await cliEvidence("inspect-native-load-image");
// Snapshot filenames are session-specific. Every measured byte/range/check is stable.
const stableImage = (value) => ({
...value,
observations: {
...value.observations,
source_files: value.observations.source_files.map(
({ name: _name, ...file }) => file,
),
},
});
assert.deepEqual(
stableImage(cliImage.normalized_result),
stableImage(image),
);
const mapped = await call("address_to_file_offset", {
address: expected.entry,
});
assert.equal(mapped.file_offset, fixture.header_bytes);
const headerMapping = await call("address_to_file_offset", {
address: "HEADER:0x0",
});
assert.equal(headerMapping.file_offset, 0);
const fileRelocation = await call("address_to_file_offset", {
address: address(fixture.relocation_offset),
});
assert.equal(
fileRelocation.file_offset,
fixture.header_bytes + fixture.relocation_offset,
);
const memoryRelocation = await call("read_bytes", {
address: address(fixture.relocation_offset),
length: 2,
});
assert.equal(memoryRelocation.bytes_hex, "0410");
assert.equal(memoryRelocation.complete, true);
assert.deepEqual(
(
await cliEvidence("read-bytes", address(fixture.relocation_offset), [
"--length",
"2",
])
).normalized_result,
memoryRelocation,
);
assert.deepEqual(
(await cliEvidence("address-to-file-offset", expected.entry))
.normalized_result,
mapped,
);
const tail = await call("read_bytes", {
address: address(fixture.module_bytes - 2),
length: 8,
});
assert.equal(tail.returned_bytes, 2);
assert.equal(tail.complete, false);
const unmapped = await call("read_bytes", {
address: "0x80000",
length: 8,
});
assert.equal(unmapped.returned_bytes, 0);
assert.equal(unmapped.complete, false);
const noSource = await client.callTool({
name: "address_to_file_offset",
arguments: { address: "0x80000" },
});
assert.equal(noSource.isError, true);
const procedures = await call("list_procedures");
assert.ok(
Array.isArray(procedures),
"Public procedure inventory is not an array",
);
const identities = Object.fromEntries(
Object.entries(expected).map(([key, value]) => {
const item = procedures.find(
(candidate) => candidate.address === value,
);
assert.ok(
item,
`Source-owned ${key} procedure ${value} was not discovered from MZ entry/call flow`,
);
return [key, item];
}),
);
const session = await call("binary_session");
const profile = session.analysis_provider_binding?.analysis_profile;
assert.ok(profile, "Public session omitted committed profile");
assertProfile(profile);
assert.equal(session.sha256, sha256);
assert.equal(session.open, true);
const facts = {};
for (const [key, identity] of Object.entries(identities)) {
const procedure = identity.address; // Use returned addresses, never symbol-name heuristics.
assert.match(procedure, /^0x[0-9a-f]+$/u);
const resolved = await call("procedure_address", { procedure });
assert.equal(resolved, procedure, "Flat address round-trip drifted");
const info = await call("procedure_info", { procedure });
const dossierResult = await evidenceCall("analyze_function", {
procedure,
detail: "complete",
});
const dossier = functionDossierSchema.parse(
dossierResult.normalized_result,
},
});
assert.deepEqual(stableImage(cliImage.normalized_result), stableImage(image));
const mapped = await call("address_to_file_offset", {
address: expected.entry,
});
assert.equal(mapped.file_offset, fixture.header_bytes);
const headerMapping = await call("address_to_file_offset", {
address: "HEADER:0x0",
});
assert.equal(headerMapping.file_offset, 0);
const fileRelocation = await call("address_to_file_offset", {
address: address(fixture.relocation_offset),
});
assert.equal(
fileRelocation.file_offset,
fixture.header_bytes + fixture.relocation_offset,
);
const memoryRelocation = await call("read_bytes", {
address: address(fixture.relocation_offset),
length: 2,
});
assert.equal(memoryRelocation.bytes_hex, "0410");
assert.equal(memoryRelocation.complete, true);
assert.deepEqual(
(
await cliEvidence("read-bytes", address(fixture.relocation_offset), [
"--length",
"2",
])
).normalized_result,
memoryRelocation,
);
assert.deepEqual(
(await cliEvidence("address-to-file-offset", expected.entry))
.normalized_result,
mapped,
);
const tail = await call("read_bytes", {
address: address(fixture.module_bytes - 2),
length: 8,
});
assert.equal(tail.returned_bytes, 2);
assert.equal(tail.complete, false);
const unmapped = await call("read_bytes", { address: "0x80000", length: 8 });
assert.equal(unmapped.returned_bytes, 0);
assert.equal(unmapped.complete, false);
const noSource = await client.callTool({
name: "address_to_file_offset",
arguments: { address: "0x80000" },
});
assert.equal(noSource.isError, true);
const procedures = await call("list_procedures");
assert.ok(
Array.isArray(procedures),
"Public procedure inventory is not an array",
);
const identities = Object.fromEntries(
Object.entries(expected).map(([key, value]) => {
const item = procedures.find((candidate) => candidate.address === value);
assert.ok(
item,
`Source-owned ${key} procedure ${value} was not discovered from MZ entry/call flow`,
);
assertBody(info.body, procedure);
assertBody(dossier.procedure.body, procedure);
assert.equal(info.entrypoint, procedure);
assert.equal(info.length, info.body.total_bytes);
assert.deepEqual(dossier.procedure.body, info.body);
const containingQueries = [procedure, info.body.ranges.at(-1).end];
for (const query of containingQueries) {
const containing = await call("resolve_containing_procedure", {
address: query,
});
assert.equal(containing.found, true);
assert.equal(containing.query_address, query);
assert.equal(containing.procedure.address, procedure);
assert.deepEqual(containing.procedure.body, info.body);
}
if (key === "entry") {
const gap = await call("resolve_containing_procedure", {
address: address(0x10),
});
assert.equal(
gap.found,
false,
"Gap inside enclosing span was treated as function body",
);
assert.equal(gap.reason, "not_in_procedure");
}
return [key, item];
}),
);
const session = await call("binary_session");
const profile = session.analysis_provider_binding?.analysis_profile;
assert.ok(profile, "Public session omitted committed profile");
assertProfile(profile);
assert.equal(session.sha256, sha256);
assert.equal(session.open, true);
const facts = {};
for (const [key, identity] of Object.entries(identities)) {
const procedure = identity.address; // Use returned addresses, never symbol-name heuristics.
assert.match(procedure, /^0x[0-9a-f]+$/u);
const resolved = await call("procedure_address", { procedure });
assert.equal(resolved, procedure, "Flat address round-trip drifted");
const info = await call("procedure_info", { procedure });
const dossierResult = await evidenceCall("analyze_function", {
procedure,
detail: "complete",
});
const dossier = functionDossierSchema.parse(
dossierResult.normalized_result,
);
assertBody(info.body, procedure);
assertBody(dossier.procedure.body, procedure);
assert.equal(info.entrypoint, procedure);
assert.equal(info.length, info.body.total_bytes);
assert.deepEqual(dossier.procedure.body, info.body);
const containingQueries = [procedure, info.body.ranges.at(-1).end];
for (const query of containingQueries) {
const containing = await call("resolve_containing_procedure", {
address: query,
});
assert.equal(containing.found, true);
assert.equal(containing.query_address, query);
assert.equal(containing.procedure.address, procedure);
assert.deepEqual(containing.procedure.body, info.body);
}
if (key === "entry") {
const gap = await call("resolve_containing_procedure", {
address: address(0x10),
});
assert.equal(
gap.found,
false,
"Gap inside enclosing span was treated as function body",
);
assert.equal(gap.reason, "not_in_procedure");
}
assert.ok(
dossier.pseudocode.trim().length > 0,
"Real DOS decompilation returned empty pseudocode",
);
assert.equal(dossier.procedure.address, procedure);
const window = await call("read_function_instructions", { procedure });
assert.deepEqual(window.procedure.body, info.body);
assert.ok(window.instructions.length > 0);
if (key === "entry") {
assert.deepEqual(info.body.ranges, [
{ start: procedure, end: address(fixture.entry_first_bytes - 1) },
{
start: address(fixture.entry_tail_offset),
end: address(
fixture.entry_tail_offset + fixture.entry_tail_bytes - 1,
),
},
]);
assert.equal(
info.body.total_bytes,
fixture.entry_first_bytes + fixture.entry_tail_bytes,
);
assert.equal(
info.body.span_bytes,
fixture.entry_tail_offset + fixture.entry_tail_bytes,
);
assert.equal(info.body.non_contiguous, true);
} else {
const size =
key === "near" ? fixture.near_body_bytes : fixture.far_body_bytes;
assert.deepEqual(info.body.ranges, [
{
start: procedure,
end: address(
(key === "near" ? fixture.near_offset : fixture.far_offset) +
size -
1,
),
},
]);
assert.equal(info.body.total_bytes, size);
assert.equal(info.body.span_bytes, size);
}
const cli = await cliEvidence("function", procedure);
if (captureDirectory !== undefined) {
await writeFile(
join(captureDirectory, `public-fixture-${key}-cli.json`),
`${JSON.stringify(cli, null, 2)}\n`,
{ mode: 0o600 },
);
await writeFile(
join(captureDirectory, `public-fixture-${key}-mcp.json`),
`${JSON.stringify(dossierResult, null, 2)}\n`,
{ mode: 0o600 },
);
}
const cliDossier = functionDossierSchema.parse(cli.normalized_result);
assert.deepEqual(
stableDossierObservations(cliDossier),
stableDossierObservations(dossier),
"CLI/MCP stable DOS dossier observations differ",
);
facts[key] = {
address: procedure,
body: info.body,
instruction_count: window.instructions.length,
pseudocode_nonempty: true,
pseudocode: dossier.pseudocode,
cli_mcp_observations_equal: true,
containing_procedure_body_equal: true,
limitations: dossier.limitations,
};
}
const move = await call("inspect_native_instruction", {
address: identities.entry.address,
});
assert.equal(move.status, "decoded");
assert.equal(move.length, 3);
assert.equal(hexBytes(move.bytes), "b83412");
assert.match(move.mnemonic, /^MOV$/iu);
assert.ok(
move.operands
.flatMap((operand) => operand.components)
.some(
(part) =>
part.kind === "register" &&
/^AX$/iu.test(part.text) &&
part.bit_width === 16,
),
"DOS entry was not decoded as 16-bit AX",
dossier.pseudocode.trim().length > 0,
"Real DOS decompilation returned empty pseudocode",
);
const nearMemory = await call("inspect_native_instruction", {
address: identities.near.address,
});
assert.equal(nearMemory.status, "decoded");
assert.equal(nearMemory.length, 2);
assert.equal(hexBytes(nearMemory.bytes), "fe00");
assert.match(nearMemory.mnemonic, /^INC$/iu);
const nearCall = await call("inspect_native_instruction", {
address: address(fixture.near_call_offset),
});
const farCall = await call("inspect_native_instruction", {
address: address(fixture.far_call_offset),
});
assert.equal(hexBytes(nearCall.bytes), "e81a00");
// Source far segment 0004 is relocated by the independently fixed 1000 load segment.
assert.equal(hexBytes(farCall.bytes), "9a00000410");
for (const [key, instruction] of [
["near", nearCall],
["far", farCall],
]) {
assert.equal(instruction.flow.kind, "call");
assert.ok(
instruction.flow.direct_destinations.includes(identities[key].address),
assert.equal(dossier.procedure.address, procedure);
const window = await call("read_function_instructions", { procedure });
assert.deepEqual(window.procedure.body, info.body);
assert.ok(window.instructions.length > 0);
if (key === "entry") {
assert.deepEqual(info.body.ranges, [
{ start: procedure, end: address(fixture.entry_first_bytes - 1) },
{
start: address(fixture.entry_tail_offset),
end: address(
fixture.entry_tail_offset + fixture.entry_tail_bytes - 1,
),
},
]);
assert.equal(
info.body.total_bytes,
fixture.entry_first_bytes + fixture.entry_tail_bytes,
);
const targets = await call("resolve_native_call_targets", {
address: instruction.address,
});
assert.equal(targets.status, "direct");
assert.ok(
targets.targets.some(
(target) => target.address === identities[key].address,
),
assert.equal(
info.body.span_bytes,
fixture.entry_tail_offset + fixture.entry_tail_bytes,
);
assert.equal(info.body.non_contiguous, true);
} else {
const size =
key === "near" ? fixture.near_body_bytes : fixture.far_body_bytes;
assert.deepEqual(info.body.ranges, [
{
start: procedure,
end: address(
(key === "near" ? fixture.near_offset : fixture.far_offset) +
size -
1,
),
},
]);
assert.equal(info.body.total_bytes, size);
assert.equal(info.body.span_bytes, size);
}
const cli = await cliEvidence("function", procedure);
if (captureDirectory !== undefined) {
await writeFile(
join(captureDirectory, `public-fixture-${key}-cli.json`),
`${JSON.stringify(cli, null, 2)}\n`,
{ mode: 0o600 },
);
await writeFile(
join(captureDirectory, `public-fixture-${key}-mcp.json`),
`${JSON.stringify(dossierResult, null, 2)}\n`,
{ mode: 0o600 },
);
}
const invalid = await client.callTool({
name: "procedure_info",
arguments: { procedure: "0xffffffffffffffff" },
});
assert.equal(invalid.isError, true, "Unmapped address did not fail");
// The CLI reads a fresh session, so compare it with MCP before the boundary
// helper's temporary rename and restoration.
const entryWindow = await call("read_function_instructions", {
procedure: identities.entry.address,
});
const cliWindow = await cliEvidence(
"instructions",
identities.entry.address,
);
assert.deepEqual(cliWindow.normalized_result, entryWindow);
await verifyGhidraAddressBoundaries(
client,
identities.entry.address,
cliEvidence,
{ mutable: !windows },
);
const entrySymbol = entryWindow.procedure.symbol;
assert.ok(entrySymbol != null, "The DOS entry needs observed symbol facts");
// Restoring the name is itself a user edit, so Ghidra records user provenance.
// Windows P0 refuses edits and never renames, so its entry stays unchanged.
const cliDossier = functionDossierSchema.parse(cli.normalized_result);
assert.deepEqual(
await call("read_function_instructions", {
procedure: identities.entry.address,
}),
windows
? entryWindow
: {
...entryWindow,
procedure: {
...entryWindow.procedure,
symbol: { ...entrySymbol, source: "user_defined" },
},
},
"Restoring the entry name changed more than its symbol provenance",
stableDossierObservations(cliDossier),
stableDossierObservations(dossier),
"CLI/MCP stable DOS dossier observations differ",
);
assert.equal(
digest(await readFile(targetPath)),
sha256,
"Analysis changed original bytes",
);
await call("close_binary");
assert.equal((await call("binary_session")).open, false);
report = {
ok: true,
mocked: false,
lane: "ghidra-dos",
host: process.platform,
provider: { id: "ghidra", version: installation.providerVersion },
target: {
format: "dos-mz",
architecture: "x86",
sha256,
bytes: fixture.bytes.length,
},
profile: {
digest: profile.digest,
loader: profile.parameters.loader,
language_id: profile.parameters.language_id,
compiler_spec_id: profile.parameters.compiler_spec_id,
load_segment: profile.parameters.load_segment,
},
transport: { cli: true, stdio_mcp: true },
load_image: {
status: image.status,
checks: image.checks.length,
mappings: image.observations.mappings.length,
relocations: image.observations.relocations.length,
cli_mcp_parity: true,
},
memory_evidence: {
relocated_word_hex: memoryRelocation.bytes_hex,
entry_file_offset: mapped.file_offset,
header_file_offset: headerMapping.file_offset,
partial_read_bytes: tail.returned_bytes,
unmapped_read_bytes: unmapped.returned_bytes,
unmapped_file_offset_rejected: true,
cli_mcp_parity: true,
},
functions: facts,
instruction_oracles: {
entry_bytes: hexBytes(move.bytes),
near_memory_bytes: hexBytes(nearMemory.bytes),
near_call_bytes: hexBytes(nearCall.bytes),
relocated_far_call_bytes: hexBytes(farCall.bytes),
near_target: expected.near,
far_target: expected.far,
},
invalid_address_rejected: true,
address_truncation_rejected: true,
original_unchanged: true,
limitations: [
"Read-only import/decompilation evidence, not DOS runtime execution or whole-program semantic equivalence.",
"The complete body is Ghidra's observed AddressSet, not independently proven historical compiler ownership.",
"Language/compiler identity is bound by the production authenticated bridge handshake; profile loader is the declared import policy.",
"Native high-pcode flow is excluded from cross-runtime parity: LOAD/STORE address-space selector constants are process-specific in current Ghidra output. Both dossiers are still strictly parsed.",
...(windows
? [
"Windows P0 is read-only: annotation and naming edits were checked as refused, not exercised.",
]
: []),
],
facts[key] = {
address: procedure,
body: info.body,
instruction_count: window.instructions.length,
pseudocode_nonempty: true,
pseudocode: dossier.pseudocode,
cli_mcp_observations_equal: true,
containing_procedure_body_equal: true,
limitations: dossier.limitations,
};
} catch (error) {
if (stderr.trim())
process.stderr.write(`DOS verifier MCP diagnostics:\n${stderr}\n`);
throw error;
}
}, [
async () => {
await closeVerifierResources([
async () => {
if (connected) await call("close_binary", {});
},
() => client.close(),
() => transport.close(),
]);
const move = await call("inspect_native_instruction", {
address: identities.entry.address,
});
assert.equal(move.status, "decoded");
assert.equal(move.length, 3);
assert.equal(hexBytes(move.bytes), "b83412");
assert.match(move.mnemonic, /^MOV$/iu);
assert.ok(
move.operands
.flatMap((operand) => operand.components)
.some(
(part) =>
part.kind === "register" &&
/^AX$/iu.test(part.text) &&
part.bit_width === 16,
),
"DOS entry was not decoded as 16-bit AX",
);
const nearMemory = await call("inspect_native_instruction", {
address: identities.near.address,
});
assert.equal(nearMemory.status, "decoded");
assert.equal(nearMemory.length, 2);
assert.equal(hexBytes(nearMemory.bytes), "fe00");
assert.match(nearMemory.mnemonic, /^INC$/iu);
const nearCall = await call("inspect_native_instruction", {
address: address(fixture.near_call_offset),
});
const farCall = await call("inspect_native_instruction", {
address: address(fixture.far_call_offset),
});
assert.equal(hexBytes(nearCall.bytes), "e81a00");
// Source far segment 0004 is relocated by the independently fixed 1000 load segment.
assert.equal(hexBytes(farCall.bytes), "9a00000410");
for (const [key, instruction] of [
["near", nearCall],
["far", farCall],
]) {
assert.equal(instruction.flow.kind, "call");
assert.ok(
instruction.flow.direct_destinations.includes(identities[key].address),
);
const targets = await call("resolve_native_call_targets", {
address: instruction.address,
});
assert.equal(targets.status, "direct");
assert.ok(
targets.targets.some(
(target) => target.address === identities[key].address,
),
);
}
const invalid = await client.callTool({
name: "procedure_info",
arguments: { procedure: "0xffffffffffffffff" },
});
assert.equal(invalid.isError, true, "Unmapped address did not fail");
// The CLI reads a fresh session, so compare it with MCP before the boundary
// helper's temporary rename and restoration.
const entryWindow = await call("read_function_instructions", {
procedure: identities.entry.address,
});
const cliWindow = await cliEvidence("instructions", identities.entry.address);
assert.deepEqual(cliWindow.normalized_result, entryWindow);
await verifyGhidraAddressBoundaries(
client,
identities.entry.address,
cliEvidence,
{ mutable: !windows },
);
const entrySymbol = entryWindow.procedure.symbol;
assert.ok(entrySymbol != null, "The DOS entry needs observed symbol facts");
// Restoring the name is itself a user edit, so Ghidra records user provenance.
// Windows P0 refuses edits and never renames, so its entry stays unchanged.
assert.deepEqual(
await call("read_function_instructions", {
procedure: identities.entry.address,
}),
windows
? entryWindow
: {
...entryWindow,
procedure: {
...entryWindow.procedure,
symbol: { ...entrySymbol, source: "user_defined" },
},
},
"Restoring the entry name changed more than its symbol provenance",
);
assert.equal(
digest(await readFile(targetPath)),
sha256,
"Analysis changed original bytes",
);
await call("close_binary");
opened = false;
assert.equal((await call("binary_session")).open, false);
report = {
ok: true,
mocked: false,
lane: "ghidra-dos",
host: process.platform,
provider: { id: "ghidra", version: installation.providerVersion },
target: {
format: "dos-mz",
architecture: "x86",
sha256,
bytes: fixture.bytes.length,
},
profile: {
digest: profile.digest,
loader: profile.parameters.loader,
language_id: profile.parameters.language_id,
compiler_spec_id: profile.parameters.compiler_spec_id,
load_segment: profile.parameters.load_segment,
},
transport: { cli: true, stdio_mcp: true },
load_image: {
status: image.status,
checks: image.checks.length,
mappings: image.observations.mappings.length,
relocations: image.observations.relocations.length,
cli_mcp_parity: true,
},
memory_evidence: {
relocated_word_hex: memoryRelocation.bytes_hex,
entry_file_offset: mapped.file_offset,
header_file_offset: headerMapping.file_offset,
partial_read_bytes: tail.returned_bytes,
unmapped_read_bytes: unmapped.returned_bytes,
unmapped_file_offset_rejected: true,
cli_mcp_parity: true,
},
functions: facts,
instruction_oracles: {
entry_bytes: hexBytes(move.bytes),
near_memory_bytes: hexBytes(nearMemory.bytes),
near_call_bytes: hexBytes(nearCall.bytes),
relocated_far_call_bytes: hexBytes(farCall.bytes),
near_target: expected.near,
far_target: expected.far,
},
invalid_address_rejected: true,
address_truncation_rejected: true,
original_unchanged: true,
limitations: [
"Read-only import/decompilation evidence, not DOS runtime execution or whole-program semantic equivalence.",
"The complete body is Ghidra's observed AddressSet, not independently proven historical compiler ownership.",
"Language/compiler identity is bound by the production authenticated bridge handshake; profile loader is the declared import policy.",
"Native high-pcode flow is excluded from cross-runtime parity: LOAD/STORE address-space selector constants are process-specific in current Ghidra output. Both dossiers are still strictly parsed.",
...(windows
? [
"Windows P0 is read-only: annotation and naming edits were checked as refused, not exercised.",
]
: []),
],
};
} catch (error) {
if (stderr.trim())
process.stderr.write(`DOS verifier MCP diagnostics:\n${stderr}\n`);
throw error;
} finally {
try {
if (opened) {
try {
await call("close_binary");
} catch (error) {
process.stderr.write(`DOS cleanup close failed: ${String(error)}\n`);
}
}
try {
await client.close();
} finally {
await transport.close();
}
assert.equal(
digest(await readFile(targetPath)),
sha256,
"Original changed during cleanup",
);
assert.deepEqual(
(await readdir(runtime)).filter((name) => name.startsWith("rea-ghidra-")),
[],
"Owned Ghidra runtime survived close",
const leftovers = (await readdir(runtime)).filter((name) =>
name.startsWith("rea-ghidra-"),
);
verifierRun = await completeVerifierRun(run);
if (process.platform !== "win32") {
assert.equal(verifierRun.process_lineage.status, "verified");
assert.deepEqual(verifierRun.process_lineage.descendants, []);
}
assert.deepEqual(leftovers, [], "Owned Ghidra runtime survived close");
} finally {
await rm(workspace, { recursive: true, force: true });
},
]);
}
}
assert.ok(report, "No DOS proof report was produced");
verifierRun = await completeVerifierRun(run);
const verifierRun = await completeVerifierRun(run);
// Windows provider jobs are verified by their native owners, as in the 6502 lane.
if (!windows) assertVerifierCleanupComplete(verifierRun.process_lineage);
process.stdout.write(
+1
View File
@@ -284,6 +284,7 @@ try {
const call = async (name, args = {}) => {
const reply = await client.callTool(
{ name, arguments: args },
undefined,
{ timeout: 180000 },
);
return requireMcpOperationResult(reply, name);
+231 -257
View File
@@ -1,8 +1,4 @@
#!/usr/bin/env node
import {
closeVerifierResources,
withVerifierCleanup,
} from "./lib/verifier-cleanup.mjs";
import assert from "node:assert/strict";
import {
requireMcpLifecycleResult,
@@ -28,8 +24,6 @@ import { parseEvidence } from "../dist/domain/evidence.js";
import { nativeDataTypeSchema } from "../dist/domain/native/nativeDataType.js";
import { nativeLoadImageSchema } from "../dist/domain/native/nativeLoadImage.js";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
const run = createVerifierRun();
const mode = process.argv[2] ?? "symbols";
if (
![
@@ -158,268 +152,248 @@ transport.stderr?.on("data", (chunk) => {
stderr = (stderr + chunk.toString()).slice(-65536);
});
let opened = false;
let connected = false;
let verifierRun;
let report;
await withVerifierCleanup(async () => {
try {
if (mode === "loader-failure") await prepareLoaderFailure();
await client.connect(transport);
connected = true;
const response = await client.callTool(
{
name: "open_binary",
arguments: { path: target, provider_id: "ghidra" },
},
{ timeout: 360000 },
);
if (
["unsupported", "malformed", "ambiguous", "loader-failure"].includes(mode)
) {
// MCP open binds a lazy session; the first inspection starts import.
let failure = response;
if (failure.isError !== true) {
opened = true;
failure = await client.callTool(
{ name: "inspect_native_load_image", arguments: {} },
{ timeout: 360000 },
);
}
assert.equal(failure.isError, true, JSON.stringify(failure));
const diagnostic = JSON.stringify(failure);
assert.match(
diagnostic,
mode === "unsupported"
? /Unsupported NativeAOT RTR format/
: mode === "malformed"
? /Reversed NativeAOT section/
: mode === "loader-failure"
? /ExceptionInInitializerError.*REA_INITIALIZER_FIXTURE/
: /Ambiguous NativeAOT directory candidates/,
);
report = { mode, target, sha256, typed_failure: failure };
} else if (mode === "default-native") {
assert.notEqual(response.isError, true, JSON.stringify(response));
try {
if (mode === "loader-failure") await prepareLoaderFailure();
await client.connect(transport);
const response = await client.callTool(
{ name: "open_binary", arguments: { path: target, provider_id: "ghidra" } },
{ timeout: 360000 },
);
if (
["unsupported", "malformed", "ambiguous", "loader-failure"].includes(mode)
) {
// MCP open binds a lazy session; the first inspection starts import.
let failure = response;
if (failure.isError !== true) {
opened = true;
const image = nativeLoadImageSchema.parse(
await call("inspect_native_load_image"),
failure = await client.callTool(
{ name: "inspect_native_load_image", arguments: {} },
{ timeout: 360000 },
);
assert.equal(image.observations.metadata_recovery, undefined);
report = { mode, target, sha256, image };
} else {
assert.notEqual(response.isError, true, JSON.stringify(response));
opened = true;
const image = nativeLoadImageSchema.parse(
await call("inspect_native_load_image"),
);
const summary = image.observations.metadata_recovery?.[0];
assert.ok(
summary,
"Loaded-image inspection omitted inline runtime-format discovery",
);
assert.equal(summary.format, "dotnet-nativeaot");
assert.equal(
summary.analysis_artifact_sha256,
hash(await readFile(env.REA_GHIDRA_NATIVEAOT_JAR)),
);
if (mode === "ordinary") {
assert.equal(summary.status, "not_applicable");
assert.equal(summary.method_tables, 0);
assert.deepEqual(summary.types, []);
report = { mode, target, sha256, summary };
} else {
assert.ok(["complete", "partial"].includes(summary.status));
assert.equal(summary.format_major, 9);
assert.equal(summary.format_minor, 1);
assert.equal(summary.types.length, summary.method_tables);
assert.ok(summary.method_tables > 0);
assert.equal(
summary.discovery,
mode === "stripped" || oracle.runtime === "win-x64"
? "signature-heuristic"
: "symbol",
);
assert.equal(summary.derived_memory.file_offset, null);
assert.ok(summary.coverage.frozen_objects_annotated > 0);
// Independent compiler/linker symbols identify fixture classes. Recovered
// Class_address names are never treated as the original source identities.
const symbols = await readFile(
join(fixtureRoot, "symbols.txt"),
"utf8",
);
const bias =
oracle.runtime === "linux-x64"
? BigInt(image.observations.image_base)
: 0n;
const addressOf = (name) => {
const line = symbols
.split(/\r?\n/u)
.find((line) => line.split(/\s+/u).includes(name));
assert.ok(line, `Independent symbol missing: ${name}`);
const match =
oracle.runtime === "linux-x64"
? line.match(/^([a-f0-9]+)\s/iu)
: line.match(/\s([a-f0-9]{16})\s/iu);
assert.ok(match, `Unrecognized independent symbol line: ${line}`);
return `0x${(BigInt(`0x${match[1]}`) + bias).toString(16)}`;
};
const fixtureSymbols =
oracle.symbols ??
(oracle.runtime === "win-x64"
? {
base: "??_7NativeAotFixture_BaseProbe@@6B@",
derived: "??_7NativeAotFixture_DerivedProbe@@6B@",
interface: "??_7NativeAotFixture_IProbe@@6B@",
compute: "NativeAotFixture_DerivedProbe__Compute",
dispatch: "NativeAotFixture_Program__Dispatch",
}
: {
base: "_ZTV26NativeAotFixture_BaseProbe",
derived: "_ZTV29NativeAotFixture_DerivedProbe",
interface: "_ZTV23NativeAotFixture_IProbe",
compute: "NativeAotFixture_DerivedProbe__Compute",
dispatch: "NativeAotFixture_Program__Dispatch",
});
const lookup = (address) => {
const identity = summary.types.find(
(type) => type.address === address,
);
assert.ok(identity?.type, `Recovered identity missing at ${address}`);
return identity.type;
};
const derivedAddress = addressOf(fixtureSymbols.derived);
const baseAddress = addressOf(fixtureSymbols.base);
const interfaceAddress = addressOf(fixtureSymbols.interface);
const derived = nativeDataTypeSchema.parse(
await call("inspect_native_data_type", {
type: lookup(derivedAddress),
}),
);
const metadata = derived.metadata_recovery;
assert.ok(metadata);
assert.equal(metadata.original_name, null);
assert.equal(metadata.name_origin, "generated");
assert.equal(metadata.method_table_address, derivedAddress);
assert.equal(metadata.related_type.address, baseAddress);
assert.ok(
metadata.interfaces.some((type) => type.address === interfaceAddress),
);
assert.ok(
metadata.virtual_slots.some(
(slot) => slot.target_address === addressOf(fixtureSymbols.compute),
),
);
const compute = await call("analyze_function", {
procedure: addressOf(fixtureSymbols.compute),
detail: "complete",
});
assert.ok(compute.pseudocode.trim());
assert.doesNotMatch(
compute.pseudocode,
/\b__thiscall\b/u,
"Upstream x64 calling-convention rewrite leaked into recovered pseudocode",
);
assert.match(compute.pseudocode, /(?:0xb|\b11\b|<<)/u);
const dispatch = await call("analyze_function", {
procedure: addressOf(fixtureSymbols.dispatch),
detail: "complete",
});
assert.ok(dispatch.pseudocode.trim());
for (const literal of oracle.expected_strings)
assert.ok(
(await call("search_strings", { pattern: literal })).length > 0,
`Frozen string missing: ${literal}`,
);
const observed = await call("read_bytes", {
address: metadata.virtual_slots[0].slot_address,
length: 8,
});
assert.equal(observed.complete, true);
assert.equal(
`0x${Buffer.from(observed.bytes_hex, "hex").readBigUInt64LE().toString(16)}`,
metadata.virtual_slots[0].target_address,
);
report = { mode, target, sha256, summary, derived, compute, dispatch };
await call("close_binary");
opened = false;
// One representative CLI boundary, selected explicitly to avoid repeated
// full NativeAOT auto-analysis during each focused iteration.
if (process.env.REA_NATIVEAOT_PROOF_CLI === "1") {
const { stdout } = await promisify(execFile)(
process.execPath,
[
entrypoint,
"inspect-native-data-type",
target,
"--type",
lookup(derivedAddress),
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 360000, maxBuffer: 16 * 1024 * 1024 },
);
const evidence = parseEvidence(JSON.parse(stdout));
assert.equal(evidence.subject.digest.sha256, sha256);
assert.deepEqual(
evidence.normalized_result.metadata_recovery,
metadata,
);
report.cli_metadata = evidence.normalized_result.metadata_recovery;
}
}
}
assert.equal(
hash(await readFile(target)),
sha256,
"Input executable changed",
assert.equal(failure.isError, true, JSON.stringify(failure));
const diagnostic = JSON.stringify(failure);
assert.match(
diagnostic,
mode === "unsupported"
? /Unsupported NativeAOT RTR format/
: mode === "malformed"
? /Reversed NativeAOT section/
: mode === "loader-failure"
? /ExceptionInInitializerError.*REA_INITIALIZER_FIXTURE/
: /Ambiguous NativeAOT directory candidates/,
);
if (opened) {
report = { mode, target, sha256, typed_failure: failure };
} else if (mode === "default-native") {
assert.notEqual(response.isError, true, JSON.stringify(response));
opened = true;
const image = nativeLoadImageSchema.parse(
await call("inspect_native_load_image"),
);
assert.equal(image.observations.metadata_recovery, undefined);
report = { mode, target, sha256, image };
} else {
assert.notEqual(response.isError, true, JSON.stringify(response));
opened = true;
const image = nativeLoadImageSchema.parse(
await call("inspect_native_load_image"),
);
const summary = image.observations.metadata_recovery?.[0];
assert.ok(
summary,
"Loaded-image inspection omitted inline runtime-format discovery",
);
assert.equal(summary.format, "dotnet-nativeaot");
assert.equal(
summary.analysis_artifact_sha256,
hash(await readFile(env.REA_GHIDRA_NATIVEAOT_JAR)),
);
if (mode === "ordinary") {
assert.equal(summary.status, "not_applicable");
assert.equal(summary.method_tables, 0);
assert.deepEqual(summary.types, []);
report = { mode, target, sha256, summary };
} else {
assert.ok(["complete", "partial"].includes(summary.status));
assert.equal(summary.format_major, 9);
assert.equal(summary.format_minor, 1);
assert.equal(summary.types.length, summary.method_tables);
assert.ok(summary.method_tables > 0);
assert.equal(
summary.discovery,
mode === "stripped" || oracle.runtime === "win-x64"
? "signature-heuristic"
: "symbol",
);
assert.equal(summary.derived_memory.file_offset, null);
assert.ok(summary.coverage.frozen_objects_annotated > 0);
// Independent compiler/linker symbols identify fixture classes. Recovered
// Class_address names are never treated as the original source identities.
const symbols = await readFile(join(fixtureRoot, "symbols.txt"), "utf8");
const bias =
oracle.runtime === "linux-x64"
? BigInt(image.observations.image_base)
: 0n;
const addressOf = (name) => {
const line = symbols
.split(/\r?\n/u)
.find((line) => line.split(/\s+/u).includes(name));
assert.ok(line, `Independent symbol missing: ${name}`);
const match =
oracle.runtime === "linux-x64"
? line.match(/^([a-f0-9]+)\s/iu)
: line.match(/\s([a-f0-9]{16})\s/iu);
assert.ok(match, `Unrecognized independent symbol line: ${line}`);
return `0x${(BigInt(`0x${match[1]}`) + bias).toString(16)}`;
};
const fixtureSymbols =
oracle.symbols ??
(oracle.runtime === "win-x64"
? {
base: "??_7NativeAotFixture_BaseProbe@@6B@",
derived: "??_7NativeAotFixture_DerivedProbe@@6B@",
interface: "??_7NativeAotFixture_IProbe@@6B@",
compute: "NativeAotFixture_DerivedProbe__Compute",
dispatch: "NativeAotFixture_Program__Dispatch",
}
: {
base: "_ZTV26NativeAotFixture_BaseProbe",
derived: "_ZTV29NativeAotFixture_DerivedProbe",
interface: "_ZTV23NativeAotFixture_IProbe",
compute: "NativeAotFixture_DerivedProbe__Compute",
dispatch: "NativeAotFixture_Program__Dispatch",
});
const lookup = (address) => {
const identity = summary.types.find((type) => type.address === address);
assert.ok(identity?.type, `Recovered identity missing at ${address}`);
return identity.type;
};
const derivedAddress = addressOf(fixtureSymbols.derived);
const baseAddress = addressOf(fixtureSymbols.base);
const interfaceAddress = addressOf(fixtureSymbols.interface);
const derived = nativeDataTypeSchema.parse(
await call("inspect_native_data_type", {
type: lookup(derivedAddress),
}),
);
const metadata = derived.metadata_recovery;
assert.ok(metadata);
assert.equal(metadata.original_name, null);
assert.equal(metadata.name_origin, "generated");
assert.equal(metadata.method_table_address, derivedAddress);
assert.equal(metadata.related_type.address, baseAddress);
assert.ok(
metadata.interfaces.some((type) => type.address === interfaceAddress),
);
assert.ok(
metadata.virtual_slots.some(
(slot) => slot.target_address === addressOf(fixtureSymbols.compute),
),
);
const compute = await call("analyze_function", {
procedure: addressOf(fixtureSymbols.compute),
detail: "complete",
});
assert.ok(compute.pseudocode.trim());
assert.doesNotMatch(
compute.pseudocode,
/\b__thiscall\b/u,
"Upstream x64 calling-convention rewrite leaked into recovered pseudocode",
);
assert.match(compute.pseudocode, /(?:0xb|\b11\b|<<)/u);
const dispatch = await call("analyze_function", {
procedure: addressOf(fixtureSymbols.dispatch),
detail: "complete",
});
assert.ok(dispatch.pseudocode.trim());
for (const literal of oracle.expected_strings)
assert.ok(
(await call("search_strings", { pattern: literal })).length > 0,
`Frozen string missing: ${literal}`,
);
const observed = await call("read_bytes", {
address: metadata.virtual_slots[0].slot_address,
length: 8,
});
assert.equal(observed.complete, true);
assert.equal(
`0x${Buffer.from(observed.bytes_hex, "hex").readBigUInt64LE().toString(16)}`,
metadata.virtual_slots[0].target_address,
);
report = { mode, target, sha256, summary, derived, compute, dispatch };
await call("close_binary");
opened = false;
// One representative CLI boundary, selected explicitly to avoid repeated
// full NativeAOT auto-analysis during each focused iteration.
if (process.env.REA_NATIVEAOT_PROOF_CLI === "1") {
const { stdout } = await promisify(execFile)(
process.execPath,
[
entrypoint,
"inspect-native-data-type",
target,
"--type",
lookup(derivedAddress),
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 360000, maxBuffer: 16 * 1024 * 1024 },
);
const evidence = parseEvidence(JSON.parse(stdout));
assert.equal(evidence.subject.digest.sha256, sha256);
assert.deepEqual(
evidence.normalized_result.metadata_recovery,
metadata,
);
report.cli_metadata = evidence.normalized_result.metadata_recovery;
}
}
} catch (error) {
console.error(stderr);
throw error;
}
}, [
async () => {
await closeVerifierResources([
async () => {
if (connected) await call("close_binary", {});
},
() => client.close(),
() => transport.close(),
]);
assert.deepEqual(
await readdir(runtime),
[],
"Owned runtime/project/extension files leaked after close",
);
verifierRun = await completeVerifierRun(run);
assert.equal(verifierRun.process_lineage.status, "verified");
assert.deepEqual(verifierRun.process_lineage.descendants, []);
await rm(workspace, { recursive: true, force: true });
},
]);
const capture = process.env.REA_NATIVEAOT_PROOF_CAPTURE_DIR;
if (capture !== undefined) {
assert.ok(isAbsolute(capture));
await mkdir(capture, { recursive: true });
await writeFile(
join(capture, `${oracle.runtime}-${mode}.json`),
JSON.stringify(report, null, 2) + "\n",
assert.equal(
hash(await readFile(target)),
sha256,
"Input executable changed",
);
if (opened) {
await call("close_binary");
opened = false;
}
await client.close();
assert.deepEqual(
await readdir(runtime),
[],
"Owned runtime/project/extension files leaked after close",
);
const capture = process.env.REA_NATIVEAOT_PROOF_CAPTURE_DIR;
if (capture !== undefined) {
assert.ok(isAbsolute(capture));
await mkdir(capture, { recursive: true });
await writeFile(
join(capture, `${oracle.runtime}-${mode}.json`),
JSON.stringify(report, null, 2) + "\n",
);
}
const outcome = report.typed_failure
? "MCP refusal"
: mode === "default-native"
? "native load image without metadata recovery"
: "MCP metadata";
console.log(
`PASS real Ghidra NativeAOT ${oracle.runtime}/${mode}: ${outcome}, source unchanged and runtime directory empty${report?.compute ? ", independent type/slot oracle and pseudocode" : ""}${report?.cli_metadata ? ", equivalent CLI" : ""}`,
);
} catch (error) {
console.error(stderr);
throw error;
} finally {
if (opened)
await client
.callTool({ name: "close_binary", arguments: {} })
.catch(() => {});
await client.close().catch(() => {});
await rm(workspace, { recursive: true, force: true });
}
const outcome = report.typed_failure
? "MCP refusal"
: mode === "default-native"
? "native load image without metadata recovery"
: "MCP metadata";
console.log(
`PASS real Ghidra NativeAOT ${oracle.runtime}/${mode}: ${outcome}, source unchanged and runtime directory empty${report?.compute ? ", independent type/slot oracle and pseudocode" : ""}${report?.cli_metadata ? ", equivalent CLI" : ""}`,
);
function hash(bytes) {
return createHash("sha256").update(bytes).digest("hex");
}
+1
View File
@@ -92,6 +92,7 @@ try {
const call = async (name, args = {}) => {
const reply = await client.callTool(
{ name, arguments: args },
undefined,
{ timeout: 180000 },
);
return requireMcpOperationResult(reply, name);
+454 -488
View File
@@ -29,10 +29,7 @@ import {
requireMcpOutput,
} from "./lib/mcp-verifier-results.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
import {
withVerifierCleanup,
closeVerifierResources,
} from "./lib/verifier-cleanup.mjs";
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
if (process.argv.length !== 2)
throw new Error("Usage: node scripts/verify-real-ghidra-raw.mjs");
@@ -105,521 +102,490 @@ let stderr = "";
transport.stderr?.on("data", (chunk) => {
stderr = (stderr + chunk.toString()).slice(-65536);
});
let connected = false;
let verifierRun;
let report;
let searchDefinition;
await withVerifierCleanup(async () => {
try {
await client.connect(transport);
connected = true;
searchDefinition = (await client.listTools()).tools.find(
(tool) => tool.name === "search_bytes",
);
assert.ok(searchDefinition?.outputSchema);
const profiles = [];
const sessions = [];
for (const entryPoint of ["0x08000000", "0x8000000", "0x09000000"]) {
// Omit provider_id to exercise reuse of the current provider route.
const opened = await call("open_binary", {
path,
format: "raw-binary",
architecture: "arm",
entry_point: entryPoint,
});
const entry = `0x${BigInt(entryPoint).toString(16)}`;
assert.equal(opened.entry_point, entry);
const image = await call("inspect_native_load_image");
assert.equal(
image.normalized_result.status,
"verified",
JSON.stringify(image),
);
assert.equal(image.normalized_result.base_address, entry);
assert.equal(image.normalized_result.entry.address, entry);
assert.equal(
image.normalized_result.observations.language_id,
"ARM:LE:32:v7",
);
assert.ok(
image.normalized_result.observations.entry_points.includes(entry),
);
assert.equal(image.analysis_profile.parameters.entry_point, entry);
profiles.push(image.analysis_profile.digest);
const status = await call("binary_session");
assert.equal(status.entry_point, entry);
sessions.push(status.analysis_run.run_id);
const memory = await call("read_bytes", {
address: entry,
length: bytes.length,
});
assert.equal(memory.normalized_result.bytes_hex, bytes.toString("hex"));
const signature = await call("search_bytes", {
pattern: "1e ?? 2f e1",
limit: 1000,
});
assert.equal(signature.normalized_result.complete, true);
assert.equal(signature.normalized_result.next_cursor, null);
assert.equal(signature.normalized_result.searched.bytes, bytes.length);
assert.deepEqual(
signature.normalized_result.matches.map((match) => match.address),
[entry],
);
assert.equal(signature.normalized_result.matches[0].file_offset, 0);
assert.equal(
signature.normalized_result.matches[0].procedure.address,
entry,
);
const procedures = await call("list_procedures");
assert.ok(
procedures.normalized_result.some(
(procedure) => procedure.address === entry,
),
);
const instruction = await call("inspect_native_instruction", {
address: entry,
});
assert.equal(instruction.normalized_result.address, entry);
assert.ok(Array.isArray(instruction.normalized_result.decoder_context));
const tmodeContext = instruction.normalized_result.decoder_context.find(
(ctx) => ctx.register === "TMode",
);
assert.ok(tmodeContext !== undefined);
assert.equal(tmodeContext.source, "ghidra-program-context");
assert.ok([null, "0", "1"].includes(tmodeContext.value));
}
assert.equal(profiles[0], profiles[1]);
assert.notEqual(profiles[1], profiles[2]);
assert.equal(sessions[0], sessions[1]);
assert.notEqual(sessions[1], sessions[2]);
await call("annotate_native_function", {
procedure: "0x9000000",
name: "retained_after_failed_open",
comment: "Owned session annotation",
try {
await client.connect(transport);
searchDefinition = (await client.listTools()).tools.find(
(tool) => tool.name === "search_bytes",
);
assert.ok(searchDefinition?.outputSchema);
const profiles = [];
const sessions = [];
for (const entryPoint of ["0x08000000", "0x8000000", "0x09000000"]) {
// Omit provider_id to exercise reuse of the current provider route.
const opened = await call("open_binary", {
path,
format: "raw-binary",
architecture: "arm",
entry_point: entryPoint,
});
const rejected = await client.callTool({
name: "open_binary",
arguments: {
path,
format: "raw-binary",
architecture: "arm64",
entry_point: "0x8000001",
},
});
assert.equal(rejected.isError, true);
assert.match(JSON.stringify(rejected.content), /must be 4-byte aligned/);
const rejection = JSON.parse(rejected.content[0].text).error;
const entry = `0x${BigInt(entryPoint).toString(16)}`;
assert.equal(opened.entry_point, entry);
const image = await call("inspect_native_load_image");
assert.equal(
rejection.details.target_at_open_completion.entry_point,
"0x9000000",
image.normalized_result.status,
"verified",
JSON.stringify(image),
);
assert.equal(rejection.details.provider_id_at_open_completion, "ghidra");
const unchanged = await call("binary_session");
assert.equal(unchanged.analysis_run.run_id, sessions[2]);
assert.equal(unchanged.entry_point, "0x9000000");
const retained = await call("analyze_function", {
procedure: "0x9000000",
detail: "complete",
});
assert.equal(image.normalized_result.base_address, entry);
assert.equal(image.normalized_result.entry.address, entry);
assert.equal(
retained.normalized_result.procedure.name,
"retained_after_failed_open",
image.normalized_result.observations.language_id,
"ARM:LE:32:v7",
);
assert.ok(
retained.normalized_result.comments.some(
(comment) => comment.text === "Owned session annotation",
image.normalized_result.observations.entry_points.includes(entry),
);
assert.equal(image.analysis_profile.parameters.entry_point, entry);
profiles.push(image.analysis_profile.digest);
const status = await call("binary_session");
assert.equal(status.entry_point, entry);
sessions.push(status.analysis_run.run_id);
const memory = await call("read_bytes", {
address: entry,
length: bytes.length,
});
assert.equal(memory.normalized_result.bytes_hex, bytes.toString("hex"));
const signature = await call("search_bytes", {
pattern: "1e ?? 2f e1",
limit: 1000,
});
assert.equal(signature.normalized_result.complete, true);
assert.equal(signature.normalized_result.next_cursor, null);
assert.equal(signature.normalized_result.searched.bytes, bytes.length);
assert.deepEqual(
signature.normalized_result.matches.map((match) => match.address),
[entry],
);
assert.equal(signature.normalized_result.matches[0].file_offset, 0);
assert.equal(
signature.normalized_result.matches[0].procedure.address,
entry,
);
const procedures = await call("list_procedures");
assert.ok(
procedures.normalized_result.some(
(procedure) => procedure.address === entry,
),
);
await call("close_binary");
const { stdout } = await promisify(execFile)(
process.execPath,
[
entrypoint,
"inspect-native-load-image",
path,
"--target-format",
"raw-binary",
"--architecture",
"arm",
"--entry-point",
"0x08000000",
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
const instruction = await call("inspect_native_instruction", {
address: entry,
});
assert.equal(instruction.normalized_result.address, entry);
assert.ok(Array.isArray(instruction.normalized_result.decoder_context));
const tmodeContext = instruction.normalized_result.decoder_context.find(
(ctx) => ctx.register === "TMode",
);
const cli = parseEvidence(JSON.parse(stdout));
assert.equal(cli.subject.digest.sha256, sha256);
assert.equal(cli.normalized_result.status, "verified");
assert.equal(cli.normalized_result.base_address, "0x8000000");
assert.equal(cli.analysis_profile.digest, profiles[0]);
assert.deepEqual(await readFile(path), bytes);
const x86Targets = [];
for (const architecture of ["x86", "x86_64"]) {
const x86Bytes = Buffer.from("b82a000000c3", "hex"); // mov eax, 42; ret.
const x86Sha256 = createHash("sha256").update(x86Bytes).digest("hex");
const x86Path = join(workspace, `${architecture}.bin`);
await writeFile(x86Path, x86Bytes);
await call("open_binary", {
path: x86Path,
format: "raw-binary",
architecture,
entry_point: "0x1000",
});
const image = await call("inspect_native_load_image", {}, x86Sha256);
assert.equal(
image.normalized_result.status,
"verified",
JSON.stringify(image),
);
assert.equal(image.normalized_result.base_address, "0x1000");
assert.equal(
image.normalized_result.observations.compiler_spec_id,
"gcc",
);
assert.equal(image.analysis_profile.parameters.compiler_spec_id, "gcc");
const memory = await call(
"read_bytes",
{ address: "0x1000", length: x86Bytes.length },
x86Sha256,
);
assert.equal(
memory.normalized_result.bytes_hex,
x86Bytes.toString("hex"),
);
const pseudo = await call(
"procedure_pseudo_code",
{ procedure: "0x1000" },
x86Sha256,
);
assert.match(pseudo.normalized_result, /\b(?:42|0x2a)\b/iu);
await call("close_binary");
assert.deepEqual(await readFile(x86Path), x86Bytes);
x86Targets.push({
architecture,
sha256: x86Sha256,
compiler_spec_id: "gcc",
return_42: true,
});
}
const repeatedPath = join(workspace, "signatures.bin");
const repeatedBytes = Buffer.from("b82a000000c3".repeat(1005), "hex");
const repeatedSha256 = createHash("sha256")
.update(repeatedBytes)
.digest("hex");
await writeFile(repeatedPath, repeatedBytes);
await call("open_binary", {
path: repeatedPath,
assert.ok(tmodeContext !== undefined);
assert.equal(tmodeContext.source, "ghidra-program-context");
assert.ok([null, "0", "1"].includes(tmodeContext.value));
}
assert.equal(profiles[0], profiles[1]);
assert.notEqual(profiles[1], profiles[2]);
assert.equal(sessions[0], sessions[1]);
assert.notEqual(sessions[1], sessions[2]);
await call("annotate_native_function", {
procedure: "0x9000000",
name: "retained_after_failed_open",
comment: "Owned session annotation",
});
const rejected = await client.callTool({
name: "open_binary",
arguments: {
path,
format: "raw-binary",
architecture: "x86_64",
architecture: "arm64",
entry_point: "0x8000001",
},
});
assert.equal(rejected.isError, true);
assert.match(JSON.stringify(rejected.content), /must be 4-byte aligned/);
const rejection = JSON.parse(rejected.content[0].text).error;
assert.equal(
rejection.details.target_at_open_completion.entry_point,
"0x9000000",
);
assert.equal(rejection.details.provider_id_at_open_completion, "ghidra");
const unchanged = await call("binary_session");
assert.equal(unchanged.analysis_run.run_id, sessions[2]);
assert.equal(unchanged.entry_point, "0x9000000");
const retained = await call("analyze_function", {
procedure: "0x9000000",
detail: "complete",
});
assert.equal(
retained.normalized_result.procedure.name,
"retained_after_failed_open",
);
assert.ok(
retained.normalized_result.comments.some(
(comment) => comment.text === "Owned session annotation",
),
);
await call("close_binary");
const { stdout } = await promisify(execFile)(
process.execPath,
[
entrypoint,
"inspect-native-load-image",
path,
"--target-format",
"raw-binary",
"--architecture",
"arm",
"--entry-point",
"0x08000000",
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
);
const cli = parseEvidence(JSON.parse(stdout));
assert.equal(cli.subject.digest.sha256, sha256);
assert.equal(cli.normalized_result.status, "verified");
assert.equal(cli.normalized_result.base_address, "0x8000000");
assert.equal(cli.analysis_profile.digest, profiles[0]);
assert.deepEqual(await readFile(path), bytes);
const x86Targets = [];
for (const architecture of ["x86", "x86_64"]) {
const x86Bytes = Buffer.from("b82a000000c3", "hex"); // mov eax, 42; ret.
const x86Sha256 = createHash("sha256").update(x86Bytes).digest("hex");
const x86Path = join(workspace, `${architecture}.bin`);
await writeFile(x86Path, x86Bytes);
await call("open_binary", {
path: x86Path,
format: "raw-binary",
architecture,
entry_point: "0x1000",
});
// Import/analysis is complete before timing the first uncached search.
await call("read_bytes", { address: "0x1000", length: 6 }, repeatedSha256);
const started = performance.now();
const page = await call(
"search_bytes",
{ pattern: "b8 ?? 00 00 00 c3", limit: 1000 },
repeatedSha256,
);
const searchElapsedMs = performance.now() - started;
assert.equal(page.normalized_result.matches.length, 1000);
assert.equal(page.normalized_result.complete, false);
const rest = await call(
"search_bytes",
{
pattern: "b8 ?? 00 00 00 c3",
limit: 1000,
cursor: page.normalized_result.next_cursor,
},
repeatedSha256,
);
assert.equal(rest.normalized_result.matches.length, 5);
const image = await call("inspect_native_load_image", {}, x86Sha256);
assert.equal(
rest.normalized_result.matches[0].address,
`0x${(0x1000 + 6000).toString(16)}`,
image.normalized_result.status,
"verified",
JSON.stringify(image),
);
assert.equal(rest.normalized_result.complete, true);
const masked = await call(
"search_bytes",
{ pattern: "bf", mask: "f0", start: "0x1000", end: "0x1005" },
repeatedSha256,
assert.equal(image.normalized_result.base_address, "0x1000");
assert.equal(image.normalized_result.observations.compiler_spec_id, "gcc");
assert.equal(image.analysis_profile.parameters.compiler_spec_id, "gcc");
const memory = await call(
"read_bytes",
{ address: "0x1000", length: x86Bytes.length },
x86Sha256,
);
assert.deepEqual(
masked.normalized_result.matches.map((match) => match.address),
["0x1000"],
assert.equal(memory.normalized_result.bytes_hex, x86Bytes.toString("hex"));
const pseudo = await call(
"procedure_pseudo_code",
{ procedure: "0x1000" },
x86Sha256,
);
const overlapping = await call(
"search_bytes",
{ pattern: "00 00", start: "0x1000", end: "0x1005" },
repeatedSha256,
);
assert.deepEqual(
overlapping.normalized_result.matches.map((match) => match.address),
["0x1002", "0x1003"],
);
const wrongCursor = await client.callTool({
name: "search_bytes",
arguments: { pattern: "c3", cursor: page.normalized_result.next_cursor },
});
assert.equal(wrongCursor.isError, true);
assert.match(pseudo.normalized_result, /\b(?:42|0x2a)\b/iu);
await call("close_binary");
assert.deepEqual(await readFile(repeatedPath), repeatedBytes);
assert.deepEqual(await readFile(x86Path), x86Bytes);
x86Targets.push({
architecture,
sha256: x86Sha256,
compiler_spec_id: "gcc",
return_42: true,
});
}
const repeatedPath = join(workspace, "signatures.bin");
const repeatedBytes = Buffer.from("b82a000000c3".repeat(1005), "hex");
const repeatedSha256 = createHash("sha256")
.update(repeatedBytes)
.digest("hex");
await writeFile(repeatedPath, repeatedBytes);
await call("open_binary", {
path: repeatedPath,
format: "raw-binary",
architecture: "x86_64",
entry_point: "0x1000",
});
// Import/analysis is complete before timing the first uncached search.
await call("read_bytes", { address: "0x1000", length: 6 }, repeatedSha256);
const started = performance.now();
const page = await call(
"search_bytes",
{ pattern: "b8 ?? 00 00 00 c3", limit: 1000 },
repeatedSha256,
);
const searchElapsedMs = performance.now() - started;
assert.equal(page.normalized_result.matches.length, 1000);
assert.equal(page.normalized_result.complete, false);
const rest = await call(
"search_bytes",
{
pattern: "b8 ?? 00 00 00 c3",
limit: 1000,
cursor: page.normalized_result.next_cursor,
},
repeatedSha256,
);
assert.equal(rest.normalized_result.matches.length, 5);
assert.equal(
rest.normalized_result.matches[0].address,
`0x${(0x1000 + 6000).toString(16)}`,
);
assert.equal(rest.normalized_result.complete, true);
const masked = await call(
"search_bytes",
{ pattern: "bf", mask: "f0", start: "0x1000", end: "0x1005" },
repeatedSha256,
);
assert.deepEqual(
masked.normalized_result.matches.map((match) => match.address),
["0x1000"],
);
const overlapping = await call(
"search_bytes",
{ pattern: "00 00", start: "0x1000", end: "0x1005" },
repeatedSha256,
);
assert.deepEqual(
overlapping.normalized_result.matches.map((match) => match.address),
["0x1002", "0x1003"],
);
const wrongCursor = await client.callTool({
name: "search_bytes",
arguments: { pattern: "c3", cursor: page.normalized_result.next_cursor },
});
assert.equal(wrongCursor.isError, true);
await call("close_binary");
assert.deepEqual(await readFile(repeatedPath), repeatedBytes);
// Two adjacent virtual regions backed by disjoint file ranges. The same
// signature crosses their boundary and overlaps at every possible start.
const splitPath = join(workspace, "split.elf");
const splitBytes = splitSearchElf(0);
const splitSha256 = createHash("sha256").update(splitBytes).digest("hex");
await writeFile(splitPath, splitBytes);
await call("open_binary", { path: splitPath, provider_id: "ghidra" });
let cursorReopened = false;
for (const pattern of ["aa aa aa", "aa ?? aa"]) {
let cursor;
const addresses = [];
const pages = [];
do {
const evidence = await call(
"search_bytes",
{
pattern,
start: "0x401000",
end: "0x401007",
limit: 2,
...(cursor === undefined ? {} : { cursor }),
},
splitSha256,
"elf",
);
const result = evidence.normalized_result;
addresses.push(...result.matches.map((match) => match.address));
pages.push({
matches: result.matches.map(({ address, segment, file_offset }) => ({
address,
segment,
file_offset,
})),
blocks: result.searched.blocks,
});
cursor = result.next_cursor ?? undefined;
if (cursor !== undefined && !cursorReopened) {
const snapshot = join(workspace, "split-snapshot.json");
await call("close_binary", { snapshot_path: snapshot });
await call("open_binary", {
path: splitPath,
provider_id: "ghidra",
snapshot_path: snapshot,
});
cursorReopened = true;
}
} while (cursor !== undefined);
assert.deepEqual(addresses, [
"0x401000",
"0x401001",
"0x401002",
"0x401003",
"0x401004",
"0x401005",
]);
assert.deepEqual(pages, [
{
matches: [
{ address: "0x401000", segment: "segment_0", file_offset: 0x1000 },
{ address: "0x401001", segment: "segment_0", file_offset: 0x1001 },
],
blocks: [
{
segment: "segment_0",
start: "0x401000",
end: "0x401003",
bytes: 4,
},
{
segment: "segment_1",
start: "0x401004",
end: "0x401004",
bytes: 1,
},
],
},
{
matches: [
{ address: "0x401002", segment: "segment_0", file_offset: 0x1002 },
{ address: "0x401003", segment: "segment_0", file_offset: 0x1003 },
],
blocks: [
{
segment: "segment_0",
start: "0x401002",
end: "0x401003",
bytes: 2,
},
{
segment: "segment_1",
start: "0x401004",
end: "0x401006",
bytes: 3,
},
],
},
{
matches: [
{ address: "0x401004", segment: "segment_1", file_offset: 0x3000 },
{ address: "0x401005", segment: "segment_1", file_offset: 0x3001 },
],
blocks: [
{
segment: "segment_1",
start: "0x401004",
end: "0x401007",
bytes: 4,
},
],
},
]);
const bounded = await call(
// Two adjacent virtual regions backed by disjoint file ranges. The same
// signature crosses their boundary and overlaps at every possible start.
const splitPath = join(workspace, "split.elf");
const splitBytes = splitSearchElf(0);
const splitSha256 = createHash("sha256").update(splitBytes).digest("hex");
await writeFile(splitPath, splitBytes);
await call("open_binary", { path: splitPath, provider_id: "ghidra" });
let cursorReopened = false;
for (const pattern of ["aa aa aa", "aa ?? aa"]) {
let cursor;
const addresses = [];
const pages = [];
do {
const evidence = await call(
"search_bytes",
{
pattern,
start: "0x401000",
end: "0x401005",
limit: 20,
end: "0x401007",
limit: 2,
...(cursor === undefined ? {} : { cursor }),
},
splitSha256,
"elf",
);
assert.deepEqual(
bounded.normalized_result.matches.map((match) => match.address),
addresses.slice(0, 4),
);
}
assert.equal(cursorReopened, true);
const fileStart = await call(
"address_to_file_offset",
{ address: "0x401000" },
splitSha256,
"elf",
);
const fileSecond = await call(
"address_to_file_offset",
{ address: "0x401004" },
splitSha256,
"elf",
);
assert.equal(fileStart.normalized_result.file_offset, 0x1000);
assert.equal(fileSecond.normalized_result.file_offset, 0x3000);
await call("close_binary");
assert.deepEqual(await readFile(splitPath), splitBytes);
await verifyCrossBlockProject(splitPath, splitSha256);
const gapPath = join(workspace, "gap.elf");
const gapBytes = splitSearchElf(1);
const gapSha256 = createHash("sha256").update(gapBytes).digest("hex");
await writeFile(gapPath, gapBytes);
await call("open_binary", { path: gapPath, provider_id: "ghidra" });
const gap = await call(
"search_bytes",
{
pattern: "aa aa aa aa ?? aa aa aa aa",
start: "0x401000",
end: "0x401008",
},
gapSha256,
"elf",
);
assert.deepEqual(gap.normalized_result.matches, []);
await call("close_binary");
assert.deepEqual(await readFile(gapPath), gapBytes);
const zeroFillPath = join(workspace, "initialized-zerofill.elf");
const zeroFillBytes = splitSearchElf(1, true);
const zeroFillSha256 = createHash("sha256")
.update(zeroFillBytes)
.digest("hex");
await writeFile(zeroFillPath, zeroFillBytes);
await call("open_binary", { path: zeroFillPath, provider_id: "ghidra" });
const zeroFill = await call(
"search_bytes",
{
pattern: "aa aa aa aa ?? aa aa aa aa",
start: "0x401000",
end: "0x401008",
},
zeroFillSha256,
"elf",
);
// This Ghidra loader initializes the ELF zero-fill byte in this fixture.
assert.deepEqual(
zeroFill.normalized_result.matches.map((match) => match.address),
["0x401000"],
);
assert.deepEqual(
zeroFill.normalized_result.searched.skipped_uninitialized,
[],
);
await call("close_binary");
assert.deepEqual(await readFile(zeroFillPath), zeroFillBytes);
report = {
ok: true,
mocked: false,
lane: "ghidra-raw",
host: process.platform,
provider: { id: "ghidra", version: installation.providerVersion },
target: {
format: "raw-binary",
architecture: "arm",
sha256,
bytes: bytes.length,
},
transport: { cli: true, stdio_mcp: true },
canonical_entry_reuses_session: true,
changed_entry_replaces_session: true,
lifecycle_entry_preserved: true,
invalid_entry_preserves_active_session: true,
load_images_verified: true,
original_unchanged: true,
x86_targets: x86Targets,
byte_search: {
cross_block_paging_and_gap: true,
matches: 1005,
page_size: 1000,
first_search_elapsed_ms: searchElapsedMs,
timing_scope: "MCP request after import; first uncached search",
token_usage: "unmeasured",
process_family_cpu_time: "unmeasured",
peak_process_family_memory: "unmeasured",
},
};
} catch (error) {
if (stderr.trim()) process.stderr.write(stderr);
throw error;
}
}, [
async () => {
await closeVerifierResources([
async () => {
if (connected) await call("close_binary", {});
},
() => client.close(),
() => transport.close(),
const result = evidence.normalized_result;
addresses.push(...result.matches.map((match) => match.address));
pages.push({
matches: result.matches.map(({ address, segment, file_offset }) => ({
address,
segment,
file_offset,
})),
blocks: result.searched.blocks,
});
cursor = result.next_cursor ?? undefined;
if (cursor !== undefined && !cursorReopened) {
const snapshot = join(workspace, "split-snapshot.json");
await call("close_binary", { snapshot_path: snapshot });
await call("open_binary", {
path: splitPath,
provider_id: "ghidra",
snapshot_path: snapshot,
});
cursorReopened = true;
}
} while (cursor !== undefined);
assert.deepEqual(addresses, [
"0x401000",
"0x401001",
"0x401002",
"0x401003",
"0x401004",
"0x401005",
]);
assert.deepEqual(pages, [
{
matches: [
{ address: "0x401000", segment: "segment_0", file_offset: 0x1000 },
{ address: "0x401001", segment: "segment_0", file_offset: 0x1001 },
],
blocks: [
{
segment: "segment_0",
start: "0x401000",
end: "0x401003",
bytes: 4,
},
{
segment: "segment_1",
start: "0x401004",
end: "0x401004",
bytes: 1,
},
],
},
{
matches: [
{ address: "0x401002", segment: "segment_0", file_offset: 0x1002 },
{ address: "0x401003", segment: "segment_0", file_offset: 0x1003 },
],
blocks: [
{
segment: "segment_0",
start: "0x401002",
end: "0x401003",
bytes: 2,
},
{
segment: "segment_1",
start: "0x401004",
end: "0x401006",
bytes: 3,
},
],
},
{
matches: [
{ address: "0x401004", segment: "segment_1", file_offset: 0x3000 },
{ address: "0x401005", segment: "segment_1", file_offset: 0x3001 },
],
blocks: [
{
segment: "segment_1",
start: "0x401004",
end: "0x401007",
bytes: 4,
},
],
},
]);
const bounded = await call(
"search_bytes",
{
pattern,
start: "0x401000",
end: "0x401005",
limit: 20,
},
splitSha256,
"elf",
);
assert.deepEqual(
(await readdir(runtime)).filter((name) => name.startsWith("rea-ghidra-")),
[],
"Owned Ghidra runtime survived close",
bounded.normalized_result.matches.map((match) => match.address),
addresses.slice(0, 4),
);
assert.equal(
fixtureCleaned,
true,
"Owned Java fixture cleanup remains pending",
);
verifierRun = await completeVerifierRun(run);
assert.equal(verifierRun.process_lineage.status, "verified");
assert.deepEqual(verifierRun.process_lineage.descendants, []);
await rm(workspace, { recursive: true, force: true });
},
]);
verifierRun = await completeVerifierRun(run);
}
assert.equal(cursorReopened, true);
const fileStart = await call(
"address_to_file_offset",
{ address: "0x401000" },
splitSha256,
"elf",
);
const fileSecond = await call(
"address_to_file_offset",
{ address: "0x401004" },
splitSha256,
"elf",
);
assert.equal(fileStart.normalized_result.file_offset, 0x1000);
assert.equal(fileSecond.normalized_result.file_offset, 0x3000);
await call("close_binary");
assert.deepEqual(await readFile(splitPath), splitBytes);
await verifyCrossBlockProject(splitPath, splitSha256);
const gapPath = join(workspace, "gap.elf");
const gapBytes = splitSearchElf(1);
const gapSha256 = createHash("sha256").update(gapBytes).digest("hex");
await writeFile(gapPath, gapBytes);
await call("open_binary", { path: gapPath, provider_id: "ghidra" });
const gap = await call(
"search_bytes",
{
pattern: "aa aa aa aa ?? aa aa aa aa",
start: "0x401000",
end: "0x401008",
},
gapSha256,
"elf",
);
assert.deepEqual(gap.normalized_result.matches, []);
await call("close_binary");
assert.deepEqual(await readFile(gapPath), gapBytes);
const zeroFillPath = join(workspace, "initialized-zerofill.elf");
const zeroFillBytes = splitSearchElf(1, true);
const zeroFillSha256 = createHash("sha256")
.update(zeroFillBytes)
.digest("hex");
await writeFile(zeroFillPath, zeroFillBytes);
await call("open_binary", { path: zeroFillPath, provider_id: "ghidra" });
const zeroFill = await call(
"search_bytes",
{
pattern: "aa aa aa aa ?? aa aa aa aa",
start: "0x401000",
end: "0x401008",
},
zeroFillSha256,
"elf",
);
// This Ghidra loader initializes the ELF zero-fill byte in this fixture.
assert.deepEqual(
zeroFill.normalized_result.matches.map((match) => match.address),
["0x401000"],
);
assert.deepEqual(
zeroFill.normalized_result.searched.skipped_uninitialized,
[],
);
await call("close_binary");
assert.deepEqual(await readFile(zeroFillPath), zeroFillBytes);
report = {
ok: true,
mocked: false,
lane: "ghidra-raw",
host: process.platform,
provider: { id: "ghidra", version: installation.providerVersion },
target: {
format: "raw-binary",
architecture: "arm",
sha256,
bytes: bytes.length,
},
transport: { cli: true, stdio_mcp: true },
canonical_entry_reuses_session: true,
changed_entry_replaces_session: true,
lifecycle_entry_preserved: true,
invalid_entry_preserves_active_session: true,
load_images_verified: true,
original_unchanged: true,
x86_targets: x86Targets,
byte_search: {
cross_block_paging_and_gap: true,
matches: 1005,
page_size: 1000,
first_search_elapsed_ms: searchElapsedMs,
timing_scope: "MCP request after import; first uncached search",
token_usage: "unmeasured",
process_family_cpu_time: "unmeasured",
peak_process_family_memory: "unmeasured",
},
};
} catch (error) {
if (stderr.trim()) process.stderr.write(stderr);
throw error;
} finally {
await client.close();
await transport.close();
if (fixtureCleaned) await rm(workspace, { recursive: true, force: true });
}
const verifierRun = await completeVerifierRun(run);
assert.equal(verifierRun.process_lineage.status, "verified");
assert.deepEqual(verifierRun.process_lineage.descendants, []);
process.stdout.write(
+15 -29
View File
@@ -1,8 +1,4 @@
#!/usr/bin/env node
import {
closeVerifierResources,
withVerifierCleanup,
} from "./lib/verifier-cleanup.mjs";
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import {
@@ -75,11 +71,9 @@ const literalObjects = ["decoder-safeguards.S", "unresolved-string-jump.s"].map(
}),
);
const reports = [];
let connected = false;
let verifierRun;
let literalReport;
let decoderCleaned = true;
await withVerifierCleanup(async () => {
try {
await execute("swiftc", ["-O", "-g", source, "-o", binary], {
timeout: 60_000,
});
@@ -116,13 +110,12 @@ await withVerifierCleanup(async () => {
);
await verifyDecoderSafeguards();
await client.connect(transport);
connected = true;
for (const [variant, path] of [
["debug", binary],
["without-dsym", copied],
]) {
await call("open_binary", { path, provider_id: "ghidra" });
await withVerifierCleanup(async () => {
try {
const procedures = await call("list_procedures", {});
const inventory = await call("analyze_swift_types", {});
assert(
@@ -190,25 +183,16 @@ await withVerifierCleanup(async () => {
metadata_accessors: metadata.total,
unclassified: inventory.unclassified.length,
});
}, [() => call("close_binary", {})]);
} finally {
await call("close_binary", {});
}
}
literalReport = await verifyStringLiterals();
}, [
async () => {
await closeVerifierResources([
async () => {
if (connected) await call("close_binary", {});
},
() => client.close(),
() => transport.close(),
]);
assert.equal(decoderCleaned, true, "Owned decoder cleanup remains pending");
verifierRun = await completeVerifierRun(run);
assert.equal(verifierRun.process_lineage.status, "verified");
assert.deepEqual(verifierRun.process_lineage.descendants, []);
await rm(root, { recursive: true, force: true });
},
]);
} finally {
await client.close();
await transport.close();
if (decoderCleaned) await rm(root, { recursive: true, force: true });
}
console.log(
JSON.stringify(
{
@@ -216,7 +200,7 @@ console.log(
provider: "ghidra",
fixtures: reports,
swift_string_literals: literalReport,
verifier_run: verifierRun,
verifier_run: await completeVerifierRun(run),
},
null,
2,
@@ -236,7 +220,7 @@ async function call(name, arguments_) {
// Literal text lives only in register constants, so it must come from the decoded pairs.
async function verifyStringLiterals() {
await call("open_binary", { path: literalBinary, provider_id: "ghidra" });
return withVerifierCleanup(async () => {
try {
const procedures = await call("list_procedures", {});
const expected = {
smallLiteral: [["SleepDisabled", "small"]],
@@ -297,7 +281,9 @@ async function verifyStringLiterals() {
decoded.unresolvedStringJump =
unresolvedDossier.swift_string_literal_omissions.length;
return decoded;
}, [() => call("close_binary", {})]);
} finally {
await call("close_binary", {});
}
}
// Exercise the Java producer inside an ephemeral Ghidra database. The explicit
+17 -16
View File
@@ -1,5 +1,4 @@
#!/usr/bin/env node
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
import assert from "node:assert/strict";
import {
requireMcpOperationResult,
@@ -96,7 +95,7 @@ await writeFile(source, buildSwitchSource());
const flags = ["-O2", "-g", "-fno-pie", "-no-pie"];
const results = [];
let productionControls = null;
await withVerifierCleanup(async () => {
try {
for (const compiler of ["gcc", "clang"]) {
const original = join(workspace, `${compiler}-optimized`);
await exec(commands[compiler], [...flags, source, "-o", original]);
@@ -170,19 +169,17 @@ await withVerifierCleanup(async () => {
});
}
}
}, [
async () => {
} finally {
try {
assert.deepEqual(
(await readdir(runtime)).filter((name) => name.startsWith("rea-ghidra-")),
[],
"Owned Ghidra runtime survived close",
);
const cleanup = await completeVerifierRun(run);
assert.equal(cleanup.process_lineage.status, "verified");
assert.deepEqual(cleanup.process_lineage.descendants, []);
} finally {
await rm(workspace, { recursive: true, force: true });
},
]);
}
}
const lifecycle = await completeVerifierRun(run);
if (lifecycle.process_lineage.status === "verified")
assert.deepEqual(lifecycle.process_lineage.descendants, []);
@@ -215,7 +212,7 @@ async function verifyTarget(path, sha256, fixtures, variant) {
let opened = false;
let stderr = "";
const observations = [];
return withVerifierCleanup(async () => {
try {
await client.connect(transport);
transport.stderr?.on("data", (chunk) => {
stderr = (stderr + chunk.toString()).slice(-65536);
@@ -290,13 +287,17 @@ async function verifyTarget(path, sha256, fixtures, variant) {
});
}
return { observations, stderr };
}, [
async () => {
} finally {
try {
if (opened) await call(client, "close_binary");
},
() => client.close(),
() => transport.close(),
]);
} finally {
try {
await client.close();
} finally {
await transport.close();
}
}
}
}
function assertWorkflowIdentity(evidence, sha256) {
assert.equal(evidence.subject.digest.sha256, sha256);
+14 -15
View File
@@ -196,6 +196,17 @@ try {
assert.equal(value.threads.length, 1);
cases++;
}
for (const [name, kind, label] of [
["aarch64-archext", 0x70000000, "PT_AARCH64_ARCHEXT"],
["aarch64-unwind", 0x70000001, "PT_AARCH64_UNWIND"],
]) {
const value = await basic.inspect(mode, fixtures[name]);
assert.equal(value.architecture, "aarch64-little-endian");
assert.equal(value.segments[0]?.type_id, kind);
assert.equal(value.segments[0]?.type, label);
assert.equal(value.threads.length, 0);
cases++;
}
const noSignals = await basic.inspect(mode, fixtures["unknown-signals"]);
assert.equal(noSignals.signals.length, 0);
assert.equal(noSignals.threads.length, 2);
@@ -205,19 +216,9 @@ try {
assert.equal(noNotes.threads.length, 0);
assert.equal(noNotes.note_interpretation_completeness, "unknown");
cases++;
for (const [name, architecture, type, typeId] of [
[
"aarch64-archext",
"aarch64-little-endian",
"PT_AARCH64_ARCHEXT",
0x70000000,
],
[
"aarch64-unwind",
"aarch64-little-endian",
"PT_AARCH64_UNWIND",
0x70000001,
],
for (const [name, architecture, type] of [
["aarch64-archext", "aarch64-little-endian", "PT_AARCH64_ARCHEXT"],
["aarch64-unwind", "aarch64-little-endian", "PT_AARCH64_UNWIND"],
["aarch64-mte-opaque", "aarch64-little-endian", 0x70000002],
["amd64-processor-opaque", "x86_64-little-endian", 0x70000001],
]) {
@@ -225,8 +226,6 @@ try {
assert.equal(auxiliary.architecture, architecture);
assert.equal(auxiliary.segments.length, 1);
assert.equal(auxiliary.segments[0].type, type);
if (typeId !== undefined)
assert.equal(auxiliary.segments[0].type_id, typeId);
assert.equal(auxiliary.notes.length, 0);
assert.equal(auxiliary.threads.length, 0);
assert.equal(auxiliary.note_interpretation_completeness, "unknown");
+10 -15
View File
@@ -1,5 +1,3 @@
import { createPackageVerifierCommands } from "./lib/verify-package-commands.mjs";
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
@@ -28,25 +26,22 @@ const environment = {
npm_config_cache: join(workspace, "npm-cache"),
};
delete environment.HOPPER_TARGET_PATH;
const commands = await createPackageVerifierCommands({
workspace: {
path: workspace,
close: () => rm(workspace, { recursive: true, force: true }),
},
});
let result;
await withVerifierCleanup(async () => {
try {
const packageRoot = await installCanaryPackage(
source,
workspace,
environment,
commands,
);
result = await verifyInstalledCanaryPackage(packageRoot, version, workspace, {
environment: environment,
commands,
});
}, [() => commands.close()]);
result = await verifyInstalledCanaryPackage(
packageRoot,
version,
workspace,
environment,
);
} finally {
await rm(workspace, { recursive: true, force: true });
}
process.stdout.write(
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), package: "rea-agents", version, source, ...result })}\n`,
);
File diff suppressed because it is too large. Load diff
@@ -66,18 +66,7 @@ try {
} else {
// Overview controls verify identity, ABI and snapshot section identities;
// they deliberately do not require any printable string values.
const section = result.libapp.sections.find(
({ name }) => name === oracle.section.name,
);
assert.ok(
section,
"Overview omitted the independently pinned snapshot section",
);
assert.deepEqual(
{ name: section.name, offset: section.offset, size: section.size },
oracle.section,
"Overview snapshot section identity differs from the independent ELF oracle",
);
assert.ok(result.libapp.sections.length > 0);
}
const tokenEstimate = calls.reduce(
(n, call) => n + call.request_token_estimate + call.reply_token_estimate,
@@ -13,28 +13,14 @@ def main():
parser.add_argument('inputs', type=Path)
parser.add_argument('output', type=Path)
options = parser.parse_args()
expected = {f'r{trial}-{task}-{budget}' for trial in [1, 2, 3]
for task in ['theme', 'ms'] for budget in ['default', 'double']}
paths = sorted(options.inputs.glob('*.metrics.json'))
observed = {path.name.removesuffix('.metrics.json') for path in paths}
if observed != expected:
raise ValueError(f'Expected the complete twelve-trial receipt set; missing={sorted(expected - observed)}, unexpected={sorted(observed - expected)}')
encoder = tiktoken.get_encoding('cl100k_base')
def compact(value):
return json.dumps(value, ensure_ascii=False, separators=(',', ':'))
rows = []
for path in paths:
for path in sorted(options.inputs.glob('*.metrics.json')):
metrics = json.loads(path.read_text())
if not (metrics['exit_code'] == 0 and metrics['stop_reason'] is None
and metrics['observer_error'] is None and metrics['cleanup'] == 'group_empty'
and metrics['remaining_group_pids'] == [] and metrics['signal_errors'] == []):
raise ValueError(f'Unsuccessful or unconfirmed observer receipt: {path.name}')
record = json.loads(path.with_name(path.name.replace('.metrics.json', '.replies.json')).read_text())
summary = json.loads(path.with_suffix('.stdout').read_text())
name = path.name.removesuffix('.metrics.json')
if (record['exit_code'] != 0 or
name != f"{name.split('-')[0]}-{summary['case']}-{summary['budget']}"):
raise ValueError(f'CLI receipt identity or completion disagrees: {path.name}')
tokens = {
'arguments': len(encoder.encode(compact(record['argv']))),
'reply': len(encoder.encode(compact(json.loads(record['stdout'])))),
+8 -15
View File
@@ -1,9 +1,9 @@
import assert from "node:assert/strict";
import { createPackageVerifierCommands } from "../../lib/verify-package-commands.mjs";
import { withVerifierCleanup } from "../../lib/verifier-cleanup.mjs";
import { execFile } from "node:child_process";
import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import {
artifactCliEvidence,
@@ -19,6 +19,8 @@ import { verifyDylibResolution } from "./macos-dylib-e2e.mjs";
import { verifyMachOOccurrences } from "./macho-occurrences.mjs";
import { verifyNativeProvenance } from "./native-provenance.mjs";
const exec = promisify(execFile);
await preflightMacosBundleFixture();
const root = await mkdtemp(join(tmpdir(), "rea-macos-bundle-"));
@@ -65,19 +67,8 @@ const EXPECTED_BUNDLES = [
["Contents/XPCServices/Svc.xpc", "xpc-service", "macos-deep", "."],
];
const commands = await createPackageVerifierCommands({
workspace: {
path: root,
close: () => rm(root, { recursive: true, force: true }),
},
});
const exec = (command, args) =>
commands.exec(command, args, {
timeout: 120_000,
maxBuffer: 16 * 1024 * 1024,
});
let report;
await withVerifierCleanup(async () => {
try {
const { app, executable } = await buildMacosBundleFixture(root);
const nativeProvenance = await verifyNativeProvenance(app, executable);
const machoOccurrences = await verifyMachOOccurrences(app, executable);
@@ -182,5 +173,7 @@ await withVerifierCleanup(async () => {
dylib_resolution: dylibs,
dyld_environment: dyldEnvironment,
};
}, [() => commands.close()]);
} finally {
await rm(root, { recursive: true, force: true });
}
process.stdout.write(`${JSON.stringify(report)}\n`);
+8 -15
View File
@@ -1,10 +1,10 @@
import assert from "node:assert/strict";
import { createPackageVerifierCommands } from "../../lib/verify-package-commands.mjs";
import { withVerifierCleanup } from "../../lib/verifier-cleanup.mjs";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import { toolContract } from "../../../dist/contracts/toolContracts.js";
import {
@@ -14,6 +14,8 @@ import {
withBundleResourcesMcp,
} from "../../lib/artifact-e2e.mjs";
const exec = promisify(execFile);
// dyld_info abbreviates a terminal .dylib suffix; padding is part of the name.
const INSTALL_NAME_CASES = [
{ installName: "leading.dylib", importSource: "leading" },
@@ -180,18 +182,7 @@ export async function verifyNativeProvenance(app, executable) {
async function verifyInstallNames() {
const root = await mkdtemp(join(tmpdir(), "rea-macho-install-names-"));
const observations = [];
const commands = await createPackageVerifierCommands({
workspace: {
path: root,
close: () => rm(root, { recursive: true, force: true }),
},
});
const exec = (command, args) =>
commands.exec(command, args, {
timeout: 120_000,
maxBuffer: 16 * 1024 * 1024,
});
return withVerifierCleanup(async () => {
try {
const librarySource = join(root, "library.c");
const consumerSource = join(root, "consumer.c");
await writeFile(librarySource, "int exported_value(void) { return 42; }\n");
@@ -288,5 +279,7 @@ async function verifyInstallNames() {
});
}
return observations;
}, [() => commands.close()]);
} finally {
await rm(root, { recursive: true, force: true });
}
}
+206 -245
View File
@@ -1,7 +1,3 @@
import {
closeVerifierResources,
withVerifierCleanup,
} from "../../lib/verifier-cleanup.mjs";
import { requireMcpToolError } from "../../lib/mcp-verifier-results.mjs";
import assert from "node:assert/strict";
import { execFile, spawn } from "node:child_process";
@@ -96,221 +92,207 @@ bridge.exposeInMainWorld("fixture", {
});
let application;
let stderr = "";
await withVerifierCleanup(async () => {
try {
await readProcessRunId(process.pid);
application = spawn(
executable,
[
"--remote-debugging-address=127.0.0.1",
"--remote-debugging-port=0",
`--user-data-dir=${join(root, "profile")}`,
main,
],
{
cwd: root,
env: { ...process.env, REA_PROCESS_RUN_ID: runId },
detached: true,
stdio: ["ignore", "ignore", "pipe"],
},
);
application.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
const endpoint = await waitForEndpoint(application, () => stderr);
await client.connect(transport);
const ajv = new Ajv2020({ strict: false, validateFormats: false });
const validators = new Map(
(await client.listTools()).tools.map((tool) => {
ajv.compile(tool.inputSchema);
return [
tool.name,
tool.outputSchema
? (tool.outputSchema.$id &&
ajv.getSchema(tool.outputSchema.$id)) ||
ajv.compile(tool.outputSchema)
: undefined,
];
}),
);
const call = async (name, arguments_) => {
console.log(`PROBE ${name}`);
const result = await client.callTool(
{ name, arguments: arguments_ },
{ timeout: 150_000 },
);
assert.notEqual(result.isError, true, JSON.stringify(result));
const validate = validators.get(name);
assert.ok(
validate?.(result.structuredContent),
JSON.stringify(validate?.errors),
);
return result.structuredContent;
};
let targets;
for (let attempt = 0; attempt < 30; attempt++) {
targets = await call("list_electron_targets", {
cdp_endpoint: endpoint,
});
if (targets.normalized_result.targets.length > 0) break;
await pause(100);
}
const target = targets.normalized_result.targets.find(
(item) => item.file_path === join(root, "renderer.html"),
);
assert.ok(target, JSON.stringify(targets));
const page = await call("inspect_electron_page", {
cdp_endpoint: endpoint,
target_id: target.target_id,
include_script_sources: true,
observation_ms: 50,
});
assert.ok(
page.normalized_result.scripts.items.some(
(script) =>
script.file_path === join(root, "renderer.js") &&
script.source.included,
),
);
const staticAnalysis = await call("analyze_javascript_application", {
input_path: root,
});
assert.equal(staticAnalysis.normalized_result.summary.browser_windows, 1);
assert.equal(
staticAnalysis.normalized_result.summary.preload_entrypoints,
1,
);
assert.equal(
staticAnalysis.normalized_result.summary.context_bridge_apis,
1,
);
assert.equal(
staticAnalysis.normalized_result.summary.ipc.main_handlers,
1,
);
assert.equal(
staticAnalysis.normalized_result.summary.ipc.renderer_transmissions,
1,
);
assert.equal(
staticAnalysis.normalized_result.summary.ipc
.paired_renderer_transmissions,
1,
);
const reconciled = await call("reconcile_javascript_runtime", {
static_layers: [
{
analysis: staticAnalysis,
runtime_mappings: [{ kind: "file-root", root }],
},
],
runtime_observations: [page],
});
assert.ok(reconciled.normalized_result.runtime_captures.length > 0);
const cli = await execute(
process.execPath,
[
entrypoint,
"inspect-electron-page",
endpoint,
target.target_id,
"--include-script-sources",
"--json",
],
{
env: { ...process.env, REA_LOG_LEVEL: "silent" },
timeout: 60_000,
maxBuffer: 32 * 1024 * 1024,
},
);
assert.equal(
JSON.parse(cli.stdout).normalized_result.target.file_path,
target.file_path,
);
const input = {
executable_path: executable,
application_path: main,
application_root: root,
actions: [{ step_id: "ipc", kind: "click", selector: "#run" }],
};
const active = await client.callTool(
{ name: "capture_electron_scenario", arguments: input },
try {
await readProcessRunId(process.pid);
application = spawn(
executable,
[
"--remote-debugging-address=127.0.0.1",
"--remote-debugging-port=0",
`--user-data-dir=${join(root, "profile")}`,
main,
],
{
cwd: root,
env: { ...process.env, REA_PROCESS_RUN_ID: runId },
detached: true,
stdio: ["ignore", "ignore", "pipe"],
},
);
application.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
const endpoint = await waitForEndpoint(application, () => stderr);
await client.connect(transport);
const ajv = new Ajv2020({ strict: false, validateFormats: false });
const validators = new Map(
(await client.listTools()).tools.map((tool) => {
ajv.compile(tool.inputSchema);
return [
tool.name,
tool.outputSchema
? (tool.outputSchema.$id && ajv.getSchema(tool.outputSchema.$id)) ||
ajv.compile(tool.outputSchema)
: undefined,
];
}),
);
const call = async (name, arguments_) => {
console.log(`PROBE ${name}`);
const result = await client.callTool(
{ name, arguments: arguments_ },
{ timeout: 150_000 },
);
const activeError = active.isError
? requireMcpToolError(active)
: undefined;
if (activeError !== undefined)
assert.equal(
activeError.code,
"cleanup_incomplete",
JSON.stringify(activeError),
);
const capture =
activeError === undefined
? active.structuredContent.normalized_result
: activeError.details?.partial_observation?.capture;
assert.ok(capture, JSON.stringify(activeError ?? active));
if (activeError !== undefined) {
assert.equal(capture.application.cleanup, "unverified");
} else {
assert.ok(
validators.get("capture_electron_scenario")(active.structuredContent),
);
assert.equal(capture.application.cleanup, "terminated-owned-process");
}
assert.equal(capture.actions[0].status, "completed");
assert.notEqual(result.isError, true, JSON.stringify(result));
const validate = validators.get(name);
assert.ok(
capture.ipc.events.some(
({ kind, channel }) =>
kind === "main-handler-invocation" && channel === "fixture:echo",
),
JSON.stringify(capture.ipc),
);
assert.equal(
capture.coverage.status,
"partial_attach",
JSON.stringify(capture),
);
assert.ok(
capture.timeline.events.some(
(event) =>
event.kind === "window-lifecycle" && event.event === "created",
),
);
await client.ping();
console.log(
`PASS live Electron CLI/MCP discovery, inspection, reconciliation, and active observations; active cleanup=${capture.application.cleanup}`,
);
if (process.platform !== "win32")
await verifyCancellation({ input, ready, entrypoint, root });
} catch (cause) {
throw new Error(
`Electron public-tool verification failed: ${cause.message}\nMCP stderr:\n${serverStderr}`,
{ cause },
validate?.(result.structuredContent),
JSON.stringify(validate?.errors),
);
return result.structuredContent;
};
let targets;
for (let attempt = 0; attempt < 30; attempt++) {
targets = await call("list_electron_targets", { cdp_endpoint: endpoint });
if (targets.normalized_result.targets.length > 0) break;
await pause(100);
}
}, [
async () => {
await closeVerifierResources([
() => client.close(),
() => transport.close(),
async () => {
if (application?.pid) {
const cleanup = await cleanupOwnedProcessGroup({
runId,
leaderPid: application.pid,
processGroupId: application.pid,
expectedParentPid: process.pid,
expectedCommand: executable,
});
assert.equal(cleanup.cleaned, true, JSON.stringify(cleanup));
}
const target = targets.normalized_result.targets.find(
(item) => item.file_path === join(root, "renderer.html"),
);
assert.ok(target, JSON.stringify(targets));
const page = await call("inspect_electron_page", {
cdp_endpoint: endpoint,
target_id: target.target_id,
include_script_sources: true,
observation_ms: 50,
});
assert.ok(
page.normalized_result.scripts.items.some(
(script) =>
script.file_path === join(root, "renderer.js") &&
script.source.included,
),
);
const staticAnalysis = await call("analyze_javascript_application", {
input_path: root,
});
assert.equal(staticAnalysis.normalized_result.summary.browser_windows, 1);
assert.equal(
staticAnalysis.normalized_result.summary.preload_entrypoints,
1,
);
assert.equal(
staticAnalysis.normalized_result.summary.context_bridge_apis,
1,
);
assert.equal(staticAnalysis.normalized_result.summary.ipc.main_handlers, 1);
assert.equal(
staticAnalysis.normalized_result.summary.ipc.renderer_transmissions,
1,
);
assert.equal(
staticAnalysis.normalized_result.summary.ipc
.paired_renderer_transmissions,
1,
);
const reconciled = await call("reconcile_javascript_runtime", {
static_layers: [
{
analysis: staticAnalysis,
runtime_mappings: [{ kind: "file-root", root }],
},
]);
await rm(root, { recursive: true, force: true });
},
]);
],
runtime_observations: [page],
});
assert.ok(reconciled.normalized_result.runtime_captures.length > 0);
const cli = await execute(
process.execPath,
[
entrypoint,
"inspect-electron-page",
endpoint,
target.target_id,
"--include-script-sources",
"--json",
],
{
env: { ...process.env, REA_LOG_LEVEL: "silent" },
timeout: 60_000,
maxBuffer: 32 * 1024 * 1024,
},
);
assert.equal(
JSON.parse(cli.stdout).normalized_result.target.file_path,
target.file_path,
);
const input = {
executable_path: executable,
application_path: main,
application_root: root,
actions: [{ step_id: "ipc", kind: "click", selector: "#run" }],
};
const active = await client.callTool(
{ name: "capture_electron_scenario", arguments: input },
{ timeout: 150_000 },
);
const activeError = active.isError
? requireMcpToolError(active)
: undefined;
if (activeError !== undefined)
assert.equal(
activeError.code,
"cleanup_incomplete",
JSON.stringify(activeError),
);
const capture =
activeError === undefined
? active.structuredContent.normalized_result
: activeError.details?.partial_observation?.capture;
assert.ok(capture, JSON.stringify(activeError ?? active));
if (activeError !== undefined) {
assert.equal(capture.application.cleanup, "unverified");
} else {
assert.ok(
validators.get("capture_electron_scenario")(active.structuredContent),
);
assert.equal(capture.application.cleanup, "terminated-owned-process");
}
assert.equal(capture.actions[0].status, "completed");
assert.ok(
capture.ipc.events.some(
({ kind, channel }) =>
kind === "main-handler-invocation" && channel === "fixture:echo",
),
JSON.stringify(capture.ipc),
);
assert.equal(
capture.coverage.status,
"partial_attach",
JSON.stringify(capture),
);
assert.ok(
capture.timeline.events.some(
(event) =>
event.kind === "window-lifecycle" && event.event === "created",
),
);
await client.ping();
console.log(
`PASS live Electron CLI/MCP discovery, inspection, reconciliation, and active observations; active cleanup=${capture.application.cleanup}`,
);
if (process.platform !== "win32")
await verifyCancellation({ input, ready, entrypoint, root });
} catch (cause) {
throw new Error(
`Electron public-tool verification failed: ${cause.message}\nMCP stderr:\n${serverStderr}`,
{ cause },
);
} finally {
await client.close();
await transport.close();
if (application?.pid) {
const cleanup = await cleanupOwnedProcessGroup({
runId,
leaderPid: application.pid,
processGroupId: application.pid,
expectedParentPid: process.pid,
expectedCommand: executable,
});
assert.equal(cleanup.cleaned, true, JSON.stringify(cleanup));
}
await rm(root, { recursive: true, force: true });
}
}
const pause = (milliseconds) =>
@@ -363,7 +345,7 @@ async function verifyCancellation({ input, ready, entrypoint, root }) {
child.once("exit", (code, signal) => resolve({ code, signal }));
});
let ownership;
await withVerifierCleanup(async () => {
try {
const deadline = Date.now() + 30_000;
while (Date.now() < deadline) {
const table = (
@@ -437,35 +419,14 @@ async function verifyCancellation({ input, ready, entrypoint, root }) {
console.log(
`PASS real Electron CLI SIGTERM, retained cancelled actions, and owned group termination; reported code=${error.code}`,
);
}, [
async () => {
for (const signal of ["SIGTERM", "SIGKILL"]) {
if (child.exitCode !== null || child.signalCode !== null) return;
child.kill(signal);
let timer;
try {
if (
await Promise.race([
exited.then(() => true),
new Promise((resolve) => {
timer = setTimeout(() => resolve(false), 2000);
}),
])
)
return;
} finally {
clearTimeout(timer);
}
}
throw new Error(
"Electron verifier CLI did not exit after SIGKILL; application cleanup remains independently owned",
);
},
async () => {
if (ownership) {
const cleanup = await cleanupOwnedProcessGroup(ownership);
assert.equal(cleanup.cleaned, true, JSON.stringify(cleanup));
}
},
]);
} finally {
if (child.exitCode === null && child.signalCode === null) {
child.kill("SIGTERM");
await exited;
}
if (ownership) {
const cleanup = await cleanupOwnedProcessGroup(ownership);
assert.equal(cleanup.cleaned, true, JSON.stringify(cleanup));
}
}
}
+18 -27
View File
@@ -1,29 +1,17 @@
import assert from "node:assert/strict";
import { createPackageVerifierCommands } from "../../lib/verify-package-commands.mjs";
import { withVerifierCleanup } from "../../lib/verifier-cleanup.mjs";
import { execFileSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
// Optional verification tool only; the runtime inspector never starts dtc.
const dtc = process.env.REA_DTC_PATH ?? "dtc";
assert.match(
execFileSync(dtc, ["--version"], { encoding: "utf8" }),
/DTC 1\.8\.1\b/,
);
const root = mkdtempSync(join(tmpdir(), "rea-dtb-verification-"));
const commands = await createPackageVerifierCommands({
workspace: {
path: root,
close: async () => rmSync(root, { recursive: true, force: true }),
},
});
const exec = async (command, args) =>
(
await commands.exec(command, args, {
timeout: 120_000,
maxBuffer: 16 * 1024 * 1024,
})
).stdout;
await withVerifierCleanup(async () => {
assert.match(await exec(dtc, ["--version"]), /DTC 1\.8\.1\b/);
try {
const source = join(root, "board.dts"),
blob = join(root, "board.dtb");
writeFileSync(
@@ -42,15 +30,16 @@ await withVerifierCleanup(async () => {
consumer { remote-endpoint = <&serial>; };
};\n`,
);
await exec(dtc, ["-I", "dts", "-O", "dtb", "-o", blob, source]);
const decompiled = await exec(dtc, ["-I", "dtb", "-O", "dts", blob]);
execFileSync(dtc, ["-I", "dts", "-O", "dtb", "-o", blob, source]);
const decompiled = execFileSync(dtc, ["-I", "dtb", "-O", "dts", blob], {
encoding: "utf8",
});
const evidence = JSON.parse(
await exec(process.execPath, [
"scripts/rea.mjs",
"inspect-devicetree",
blob,
"--json",
]),
execFileSync(
process.execPath,
["scripts/rea.mjs", "inspect-devicetree", blob, "--json"],
{ encoding: "utf8" },
),
);
const report = evidence.normalized_result;
// This controlled fixture uses one opening/closing brace per node line.
@@ -94,4 +83,6 @@ await withVerifierCleanup(async () => {
2,
),
);
}, [() => commands.close()]);
} finally {
rmSync(root, { recursive: true, force: true });
}
@@ -1,7 +1,3 @@
import {
closeVerifierResources,
withVerifierCleanup,
} from "../../lib/verifier-cleanup.mjs";
import assert from "node:assert/strict";
import { execFile, spawn } from "node:child_process";
import { once } from "node:events";
@@ -42,7 +38,7 @@ const names = [
...(process.platform === "win32" ? [] : ['with"quote.cjs']),
];
await withVerifierCleanup(async () => {
try {
await client.connect(transport);
for (const name of names) {
const targetPath =
@@ -58,20 +54,19 @@ await withVerifierCleanup(async () => {
stdio: ["ignore", "ignore", "pipe"],
},
);
await withVerifierCleanup(
() => verifyTarget(child, targetPath, name),
[() => terminate(child)],
);
try {
await verifyTarget(child, targetPath, name);
} finally {
await terminate(child);
}
}
}, [
async () => {
await closeVerifierResources([
() => client.close(),
() => transport.close(),
]);
} finally {
try {
await client.close();
} finally {
await rm(root, { recursive: true, force: true });
},
]);
}
}
async function verifyTarget(child, targetPath, name) {
const socket = new URL(await inspectorUrl(child));
@@ -197,37 +192,15 @@ async function runMcp(name, arguments_) {
}
async function inspectorUrl(process_) {
return new Promise((resolveUrl, rejectUrl) => {
let captured = "";
const timer = setTimeout(
() =>
finish(new Error("Inspector startup exceeded its 10000ms deadline")),
10_000,
);
const finish = (error, url) => {
clearTimeout(timer);
process_.stderr.off("data", onData);
process_.stderr.off("end", onEnd);
process_.stderr.off("error", onError);
process_.off("error", onError);
if (error !== undefined) rejectUrl(error);
else resolveUrl(url);
};
const onData = (chunk) => {
captured += chunk.toString("utf8");
const match = /Debugger listening on (ws:\/\/[^\s]+)/u.exec(captured);
if (match?.[1] !== undefined) finish(undefined, match[1]);
else if (captured.length > 64 * 1_024)
finish(new Error("Inspector startup output exceeded its bound"));
};
const onEnd = () =>
finish(new Error("Node exited before reporting its Inspector URL"));
const onError = (error) => finish(error);
process_.stderr.on("data", onData);
process_.stderr.once("end", onEnd);
process_.stderr.once("error", onError);
process_.once("error", onError);
});
let captured = "";
for await (const chunk of process_.stderr) {
captured += chunk.toString("utf8");
const match = /Debugger listening on (ws:\/\/[^\s]+)/u.exec(captured);
if (match?.[1] !== undefined) return match[1];
if (captured.length > 64 * 1_024)
throw new Error("Inspector startup output exceeded its bound");
}
throw new Error("Node exited before reporting its Inspector URL");
}
async function runCli(arguments_) {
@@ -263,14 +236,7 @@ async function terminate(process_) {
)
return;
process_.kill("SIGKILL");
clearTimeout(timer);
const killed = await Promise.race([
exited.then(() => true),
new Promise((resolveTimeout) => {
timer = setTimeout(() => resolveTimeout(false), 2_000);
}),
]);
assert.equal(killed, true, "Inspector fixture did not exit after SIGKILL");
await exited;
} finally {
clearTimeout(timer);
}
+6 -14
View File
@@ -1,8 +1,4 @@
#!/usr/bin/env node
import {
closeVerifierResources,
withVerifierCleanup,
} from "../../lib/verifier-cleanup.mjs";
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
@@ -55,7 +51,7 @@ const call = async (name, args) => {
const fingerprint = (bytes) => createHash("sha256").update(bytes).digest("hex");
const counts = [];
let finalReport;
await withVerifierCleanup(async () => {
try {
const fixtures = await buildRecoveryFixtures(
root,
process.env.REA_JAVASCRIPT_FIXTURE_TOOLS,
@@ -169,15 +165,11 @@ await withVerifierCleanup(async () => {
results: counts,
verified: true,
};
}, [
async () => {
await closeVerifierResources([
() => client.close(),
() => transport.close(),
]);
await rm(root, { recursive: true, force: true });
},
]);
} finally {
await client.close();
await transport.close();
await rm(root, { recursive: true, force: true });
}
console.log(JSON.stringify(finalReport));
async function assertRecoveredArtifacts(evidence, result, fixture) {
+36 -101
View File
@@ -32,61 +32,50 @@ async function runManagedAppManifestSelfTest(context) {
const members = inspectManagedMembersBytes(sample.bytes, sample.target);
const method = members.methods[0];
assert.ok(method);
const manifest = {
label: "source-owned manifest verifier self-test",
target: {
path: sample.path,
sha256: sample.target.sha256,
mvid: artifact.module?.mvid,
assembly_name: artifact.assembly?.name,
runtime_family: artifact.classification.runtime_family,
managed_architecture: artifact.classification.managed_architecture,
},
methods: [
{
label: "pinned-semantic-slice",
token: method.token,
signature_sha256: method.signature.raw_sha256,
il_size: method.body.il_size,
il_sha256: method.body.il_sha256,
normalized_il_sha256: method.body.normalized_il_sha256,
const summary = await context.verifyManagedAppManifest(
{
label: "source-owned manifest verifier self-test",
target: {
path: sample.path,
sha256: sample.target.sha256,
mvid: artifact.module?.mvid,
assembly_name: artifact.assembly?.name,
runtime_family: artifact.classification.runtime_family,
managed_architecture: artifact.classification.managed_architecture,
},
],
application_graph: {
expected_node_kinds: [
"artifact",
"managed-assembly",
"managed-module",
"managed-type",
"managed-method",
],
feature_traces: [
methods: [
{
label: "trace pinned method by name",
method_token: method.token,
seed: "PinnedSemanticSlice",
match: "exact",
min_matched_seeds: 1,
label: "pinned-semantic-slice",
token: method.token,
signature_sha256: method.signature.raw_sha256,
il_size: method.body.il_size,
il_sha256: method.body.il_sha256,
normalized_il_sha256: method.body.normalized_il_sha256,
},
],
application_graph: {
expected_node_kinds: [
"artifact",
"managed-assembly",
"managed-module",
"managed-type",
"managed-method",
],
feature_traces: [
{
label: "trace pinned method by name",
method_token: method.token,
seed: "PinnedSemanticSlice",
match: "exact",
min_matched_seeds: 1,
},
],
},
},
};
const manifestPath = join(
context.workspace,
"source-owned-managed-app-manifest.json",
);
const summary = await context.verifyManagedAppManifest(
manifest,
manifestPath,
);
const rejectedMismatches = await verifyManifestRejections(
context,
manifest,
manifestPath,
join(context.workspace, "source-owned-managed-app-manifest.json"),
);
return {
verified: true,
rejected_mismatches: rejectedMismatches,
assertions: summary.assertions,
methods: summary.methods.length,
applicationGraph: summary.application_graph ?? null,
@@ -94,60 +83,6 @@ async function runManagedAppManifestSelfTest(context) {
};
}
async function verifyManifestRejections(context, manifest, manifestPath) {
const refusals = [
[
"target sha256",
["target", "sha256"],
"0".repeat(64),
/target sha256 mismatch/u,
],
[
"method token",
["methods", 0, "token"],
"0x0600ffff",
/method 0x0600ffff was not returned/u,
],
[
"signature sha256",
["methods", 0, "signature_sha256"],
"0".repeat(64),
/signature sha256 mismatch/u,
],
["IL size", ["methods", 0, "il_size"], 999, /IL size mismatch/u],
[
"IL sha256",
["methods", 0, "il_sha256"],
"0".repeat(64),
/IL sha256 mismatch/u,
],
[
"normalized IL sha256",
["methods", 0, "normalized_il_sha256"],
"0".repeat(64),
/normalized IL sha256 mismatch/u,
],
];
for (const [label, path, wrong, expected] of refusals) {
const rejected = structuredClone(manifest);
let owner = rejected;
for (const key of path.slice(0, -1)) owner = owner[key];
const key = path.at(-1);
assert.notEqual(
owner[key],
wrong,
`${label} control must differ from the valid manifest`,
);
owner[key] = wrong;
await assert.rejects(
() => context.verifyManagedAppManifest(rejected, manifestPath),
expected,
`Manifest verifier accepted an authored wrong ${label}`,
);
}
return refusals.map(([label]) => label);
}
async function runOptionalManagedAppManifest(context) {
const manifestPath = process.env.REA_MANAGED_APP_MANIFEST_PATH;
if (manifestPath === undefined) return null;
+28 -45
View File
@@ -779,40 +779,20 @@ if [ "$1" = "version" ]; then ${VERSION_BLOCK}; fi`,
});
it("reads a bounded logcat dump with the caller count and buffer", async () => {
const { binary, root } = await writeAdbStub(
(root) => `printf '%s\\n' "$@" >> '${join(root, "argv.log")}'
${shellStub({
logcat:
"08-10 13:00:00.000 I/Tag( 123): first\n08-10 13:00:01.000 I/Tag( 123): second\n",
})(root)}`,
const { binary } = await writeAdbStub(
shellStub({
logcat:
"08-10 13:00:00.000 I/Tag( 123): first\n08-10 13:00:01.000 I/Tag( 123): second\n",
}),
);
const result = await evidenceResult(providerOver(binary), {
operation: "read_adb_logcat",
input: { serial: "emulator-5554", count: 1, buffer: "system", pid: 123 },
input: { serial: "emulator-5554", count: 500, buffer: "main" },
});
expect(
(await readFile(join(root, "argv.log"), "utf8")).trim().split("\n"),
).toEqual([
"version",
"-s",
"emulator-5554",
"shell",
"logcat",
"-d",
"-b",
"system",
"-t",
"1",
"--pid",
"123",
expect(result.lines).toEqual([
"08-10 13:00:00.000 I/Tag( 123): first",
"08-10 13:00:01.000 I/Tag( 123): second",
]);
expect(result).toMatchObject({
count: 1,
buffer: "system",
pid: 123,
coverage: "partial",
lines: ["08-10 13:00:01.000 I/Tag( 123): second"],
});
});
it("reports UTF-8 byte counts in total_bytes for non-ASCII logcat output", async () => {
@@ -1687,15 +1667,13 @@ exit 1`,
operation: "capture_adb_screen",
input: { serial: "emulator-5554", output_directory: output },
});
expect(result).toMatchObject({
width: 1,
height: 1,
bytes: SCREEN_PNG.byteLength,
sha256: createHash("sha256").update(SCREEN_PNG).digest("hex"),
});
expect((await readFile(join(output, "screen.png"))).equals(SCREEN_PNG)).toBe(
true,
);
expect(result).toMatchObject({ width: 1, height: 1 });
expect(
await readFile(join(output, "screen.png")).then(
(bytes) => bytes.subarray(1, 4).toString("latin1") === "PNG",
() => false,
),
).toBe(true);
});
it("stops a screen capture aborted after adb returns but before publication", async () => {
@@ -1911,12 +1889,11 @@ it("reads Android 16 focus declarations from displays when windows omits them",
"mCurrentFocus=Window{abc u0 com.example/.Main}\nmFocusedApp=ActivityRecord{def u0 com.example/.Main t1}\n",
}),
);
const execution = await providerOver(binary).execute({
const result = await evidenceResult(providerOver(binary), {
operation: "inspect_adb_window",
input: { serial: "emulator-5554" },
});
if (!execution.ok) throw execution.error;
expect(execution.value.result).toMatchObject({
expect(result).toMatchObject({
focused_window: "Window{abc u0 com.example/.Main}",
focused_app: "ActivityRecord{def u0 com.example/.Main t1}",
observed_lines: [
@@ -1924,6 +1901,11 @@ it("reads Android 16 focus declarations from displays when windows omits them",
"mFocusedApp=ActivityRecord{def u0 com.example/.Main t1}",
],
});
const execution = await providerOver(binary).execute({
operation: "inspect_adb_window",
input: { serial: "emulator-5554" },
});
if (!execution.ok) throw execution.error;
expect(execution.value.limitations).toContainEqual(
expect.stringContaining("dumpsys window displays"),
);
@@ -1936,15 +1918,16 @@ it("describes missing window focus as unknown after both sections omit declarati
"dumpsys:window:displays": "no focus\\n",
}),
);
const result = await evidenceResult(providerOver(binary), {
operation: "inspect_adb_window",
input: { serial: "emulator-5554" },
});
expect(result).toMatchObject({ focused_window: null, focused_app: null });
const execution = await providerOver(binary).execute({
operation: "inspect_adb_window",
input: { serial: "emulator-5554" },
});
if (!execution.ok) throw execution.error;
expect(execution.value.result).toMatchObject({
focused_window: null,
focused_app: null,
});
expect(execution.value.limitations).toContainEqual(
expect.stringContaining("focus is unknown"),
);
+10
View File
@@ -235,6 +235,16 @@ describe("parseWindowFocusOutput declaration presence", () => {
});
describe("parseDumpsysPackageOutput", () => {
it.each(["userId", "appId"])(
"retains the printed %s package identifier",
(key) => {
expect(
parseDumpsysPackageOutput(
`Packages:\n Package [com.example] (abcdef):\n ${key}=10234\n`,
).userId,
).toBe(10234);
},
);
it("reports unknown requested permissions when the package is null", () => {
expect(
parseDumpsysPackageOutput("Package [com.example] (abcdef):\n pkg=null\n")
+2 -1
View File
@@ -30,6 +30,7 @@ import {
jadxXrefsSchema,
normalizeJadxText,
} from "./JadxProtocol.js";
import { appendAll } from "../domain/arrayAppend.js";
/** The adapter consumes tool payloads, never provider RPC details outside this layer. */
export interface JadxToolPort {
@@ -336,7 +337,7 @@ const searchClasses = async (
request.operation,
"JADX class pagination is incomplete",
);
matches.push(...page.items);
appendAll(matches, page.items);
offset = page.next_offset;
} while (offset < loaded.class_count);
const occurrences = new Map<string, number>();
+2 -1
View File
@@ -24,6 +24,7 @@ import {
import { androidSnapshotCopy } from "./AndroidTargetSnapshot.js";
import type { JadxSession } from "./JadxSession.js";
import type { JadxMcpTransport } from "./JadxMcpTransport.js";
import { appendAll } from "../domain/arrayAppend.js";
interface FailureContext {
readonly cause: unknown;
@@ -280,7 +281,7 @@ const snapshotCloseFailures = (
cause.outcome.kind === "failed" &&
cause.outcome.cause instanceof RegularFileCleanupFailure
)
failures.push(...snapshotCloseFailures(cause.outcome.cause));
appendAll(failures, snapshotCloseFailures(cause.outcome.cause));
return failures;
};
+4 -9
View File
@@ -2,6 +2,7 @@ import { expect, it } from "vitest";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { captureJadxFailure } from "./JadxFailure.js";
import { jadxOperationTimeoutGuidance } from "./JadxOperationTimeout.js";
it("names REA_JADX_OPERATION_TIMEOUT_MS when the operation deadline elapses", () => {
const timeout = AbortSignal.abort(new Error("deadline"));
@@ -17,15 +18,9 @@ it("names REA_JADX_OPERATION_TIMEOUT_MS when the operation deadline elapses", ()
session: undefined,
});
const projected = projectAnalysisError(present());
expect(projected.message).toContain("1500 ms");
expect(projected.remediation.action).toContain(
"REA_JADX_OPERATION_TIMEOUT_MS",
);
expect(projected.remediation.action).toContain("update_analysis_settings");
expect(projected.remediation.action).toContain("jadx.operation_timeout_ms");
expect(projected.remediation.action).toContain(
"next operation without restarting REA",
);
const guidance = jadxOperationTimeoutGuidance(1500);
expect(projected.message).toBe(guidance.userMessage);
expect(projected.remediation.action).toBe(guidance.remediationAction);
expect(projected.details).toMatchObject({
limit: {
setting: "jadx.operation_timeout_ms",
+30 -6
View File
@@ -686,20 +686,44 @@ it("projects one locale's strings on request", async () => {
]);
});
it("notes a missing locale table instead of failing", async () => {
const { command, root } = await writeApktoolStub(decodeStub());
const apk = await apkFixture(root);
const result = await evidenceResult(providerOver(command), {
operation: "decode_android_resources",
input: { path: apk.path, include_strings: true, locale: "fr" },
});
expect(result.strings).toEqual([]);
});
it("notes a missing default strings table without strings", async () => {
const { command, root } = await writeApktoolStub(
decodeStub({ withStrings: false }),
);
const apk = await apkFixture(root);
const execution = await providerOver(command).execute({
const result = await evidenceResult(providerOver(command), {
operation: "decode_android_resources",
input: { path: apk.path, include_strings: true },
});
if (!execution.ok) throw execution.error;
expect(execution.value.result).toMatchObject({ strings: [] });
expect(execution.value.limitations).toContain(
"No default res/values/strings.xml was found in the decoded workspace",
expect(result.strings).toEqual([]);
});
it("reports apktool's own refusal for undecodable targets", async () => {
const { command, root } = await writeApktoolStub(
decodeStub({ failDecode: true }),
);
const apk = await apkFixture(root);
const projected = await refusedBy(providerOver(command), {
operation: "decode_android_resources",
input: { path: apk.path, include_strings: true },
});
expect(projected).toMatchObject({
code: "unsupported_target",
details: { path: apk.path },
});
expect(projected.message).toContain(apk.path);
expect(projected.message).not.toContain(command);
expect(projected.message).toContain("Could not decode");
});
it("reports a missing launcher as a capability gap with recovery", async () => {
@@ -1501,7 +1525,6 @@ it.each(["first-selected.apk", "second-selected.apk"])(
details: { path, captured_output: { exit_code: 1 } },
});
expect(projected.message).toContain(path);
expect(projected.message).toContain("Could not decode");
expect(projected.message).not.toContain(command);
} finally {
await provider.close();
@@ -1654,6 +1677,7 @@ it.each([
expect(warnings.length).toBe(
["2.7.0-dirty", "2.9.1", "2.9.2-rc.1"].includes(version) ? 1 : 0,
);
if (warnings.length > 0) expect(warnings[0]).toContain("CVE-2024-21633");
expect(
inspect.value.limitations.filter((warning) =>
warning.includes("CVE-2026-39973"),
+2 -1
View File
@@ -71,6 +71,7 @@ import {
parseApktoolYml,
parseManifestPackage,
} from "./ApktoolDecodeOutput.js";
import { appendAll } from "../domain/arrayAppend.js";
const DECODE_MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
const MANIFEST_MAX_BYTES = 512 * 1024;
@@ -657,7 +658,7 @@ export class ApktoolProvider {
const projectedStrings = projection.ok
? projection.value
: projection.error.partial;
limitations.push(...projectedStrings.limitations);
appendAll(limitations, projectedStrings.limitations);
const coverage =
projectedStrings.coverage === "partial" ||
manifestRead.truncated ||
+65 -27
View File
@@ -23,6 +23,7 @@ import {
type AnalysisError,
} from "../domain/analysisErrorBase.js";
import type { Result } from "../domain/result.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { err, ok } from "../domain/result.js";
const provider = { id: "fixture", name: "Fixture", version: "1" };
@@ -136,6 +137,52 @@ describe("analysis context queries: navigation", () => {
);
});
it.each<JsonValue>([
null,
{},
{ found: false, procedure: null },
{ query_address: "0x401000", found: true, procedure: null },
])(
"keeps malformed containing-procedure output unknown: %j",
async (value) => {
const analysis: AnalysisOperationPort = {
execute: async (operation) =>
ok(
createAnalysisExecution(
operation === "resolve_containing_procedure"
? value
: operation === "current_document"
? "fixture"
: "0x401000",
provider,
),
),
};
expect(await getNavigationContext(analysis, {})).toMatchObject({
ok: false,
error: {
_tag: "AnalysisOutputError",
operation: "resolve_containing_procedure",
},
});
},
);
it("does not infer procedure absence from a provider error message", async () => {
const failure = new AnalysisProtocolError(
"not in a procedure: malformed reply",
);
const analysis: AnalysisOperationPort = {
execute: async (operation) =>
operation === "resolve_containing_procedure"
? err(failure)
: ok(createAnalysisExecution("0x401000", provider)),
};
expect(
await getNavigationContext(analysis, { document: "fixture" }),
).toEqual(err(failure));
});
it("uses an explicit document without passing it to current_document and preserves procedure failures", async () => {
const calls: string[] = [];
const failure = new AnalysisProtocolError("provider response malformed");
@@ -162,14 +209,6 @@ describe("navigation procedure admission", () => {
it.each([
{ condition: "missing result", value: null },
{ condition: "missing discriminator", value: {} },
{
condition: "missing absence reason",
value: { found: false, procedure: null },
},
{
condition: "found procedure is null",
value: { query_address: "0x401000", found: true, procedure: null },
},
{
condition: "missing query identity",
value: { found: false, procedure: null, reason: "not_in_procedure" },
@@ -207,25 +246,24 @@ describe("navigation procedure admission", () => {
},
);
it.each([
"Provider failed: no procedure exists",
"Provider failed: not in a procedure",
"not in a procedure: malformed reply",
])("preserves the original provider failure: %s", async (message) => {
const failure = new AnalysisProtocolError(message);
const result = await getNavigationContext(
{
execute: async (operation) =>
operation === "current_address"
? ok(createAnalysisExecution("0x401000", provider))
: err(failure),
},
{ document: "fixture" },
);
expect(result).toEqual(err(failure));
if (result.ok) throw new Error("expected original provider failure");
expect(result.error).toBe(failure);
});
it.each(["no procedure exists", "not in a procedure"])(
"preserves a provider failure containing %s",
async (message) => {
const failure = new AnalysisProtocolError(`Provider failed: ${message}`);
const result = await getNavigationContext(
{
execute: async (operation) =>
operation === "current_address"
? ok(createAnalysisExecution("0x401000", provider))
: err(failure),
},
{ document: "fixture" },
);
expect(result).toEqual(err(failure));
if (result.ok) throw new Error("expected original provider failure");
expect(result.error).toBe(failure);
},
);
it("returns a validated containing procedure name", async () => {
const result = await getNavigationContext(
-3
View File
@@ -97,7 +97,6 @@ import {
isDfuSeFile,
} from "../domain/firmware/dfuse.js";
import { hasFitImagesNode } from "../domain/firmware/fitImage.js";
import { hasMftFileRecordHeader } from "../domain/windows/ntfsMft.js";
import {
locateEspFlashLayout,
type EspFlashSelection,
@@ -817,7 +816,6 @@ const detectArtifactFormat = async (
});
if (esp === "esp-image") return "esp-image";
if (hasFitImagesNode(magic)) return "fit";
if (hasMftFileRecordHeader(magic)) return "ntfs-mft";
// Binary plists, including keyed archives, often use other extensions.
if (hasBinaryPlistSignature(magic)) return "plist";
const named = namedArtifactFormat(lower);
@@ -901,7 +899,6 @@ const isArchiveFormat = (
"dfuse",
"esp-flash-image",
"fit",
"ntfs-mft",
].includes(format);
/** Whether the leading bytes begin any executable header family, valid or not. */
+2 -1
View File
@@ -1,4 +1,5 @@
import { PCAP_CAPTURE_UNSUPPORTED_HOST_REASON } from "../domain/pcap/pcapAnalysis.js";
import { appendAll } from "../domain/arrayAppend.js";
import { DEFAULT_CALL_RELATIONSHIP_DIRECTION } from "../contracts/enhancedInputs.js";
import {
FIRMWARE_OPERATION_ENGINES,
@@ -1180,7 +1181,7 @@ const reachableRequirements = (
descriptor.runtime_unavailable === true)
);
});
reached.push(...available);
appendAll(reached, available);
if (available.length === 0) break;
}
return reached;
@@ -11,6 +11,7 @@ import {
type RegularFileOperationOutcome,
} from "../filesystem/OwnedRegularFile.js";
import { retryRegularFileCleanupFailures } from "./RegularFileRead.js";
import { appendAll } from "../domain/arrayAppend.js";
/** Completion and exact staging owner retained when publication cleanup fails. */
export class ClientConfigurationPublicationFailure<
@@ -72,7 +73,7 @@ export const retryClientConfigurationCleanup = async (
let nested: unknown;
if (current instanceof RegularFileCleanupFailure) {
const retried = await retryRegularFileCleanupFailures(current, resources);
failures.push(...retried.failures);
appendAll(failures, retried.failures);
for (const lease of retried.cleanupLeases) cleanupLeases.add(lease);
if (retried.cleanup !== undefined) observations.push(retried.cleanup);
primary = retried.primary;
+1 -1
View File
@@ -389,7 +389,7 @@ const providerSettingSelections = (
];
/** Render what the registration comparison established without inferring its effective environment. */
const registrationEnvironmentDetail = (
export const registrationEnvironmentDetail = (
client: string,
{
unrecorded,
+3 -2
View File
@@ -11,6 +11,7 @@ import type {
DoctorHost,
DoctorProviderInspection,
} from "./Doctor.js";
import { appendAll } from "../domain/arrayAppend.js";
/** Ordered environment diagnostics consumed by the Doctor report assembler. */
export interface DoctorDiagnostics {
@@ -32,13 +33,13 @@ export const collectDoctorDiagnostics = async (
checks.push(hopperCheck(host, hopperPath));
const hopperTimeoutCheck = optionalHopperStartupTimeoutCheck(host);
if (hopperTimeoutCheck !== undefined) checks.push(hopperTimeoutCheck);
checks.push(...runtimeConfigurationChecks(host));
appendAll(checks, runtimeConfigurationChecks(host));
const hopperVersionCheck = await optionalHopperVersionCheck(host, hopperPath);
if (hopperVersionCheck !== undefined) checks.push(hopperVersionCheck);
const providerInspections = await host.providerInspections?.(clients ?? []);
if (providerInspections !== undefined)
checks.push(...providerInspections.flatMap(providerDoctorChecks));
appendAll(checks, providerInspections.flatMap(providerDoctorChecks));
const ilspyCheck = await optionalIlspyCheck(host);
if (ilspyCheck !== undefined) checks.push(ilspyCheck);
@@ -4,7 +4,12 @@ import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import { createDoctorHostFixture as host } from "./Doctor.fixture.js";
import { runDoctor, systemDoctorHost, type DoctorHost } from "./Doctor.js";
import {
registrationEnvironmentDetail,
runDoctor,
systemDoctorHost,
type DoctorHost,
} from "./Doctor.js";
const inspection = (
id: string,
@@ -181,12 +186,11 @@ it("states each comparison result without a claim it did not establish", async (
classification: "unknown",
details: { effective_environment: "unknown" },
});
expect(check?.detail).toContain(
"what the server cursor starts receives is unknown",
expect(check?.detail).toBe(
registrationEnvironmentDetail("cursor", {
unrecorded: [],
differing: ["JAVA_HOME"],
unresolved: ["GHIDRA_INSTALL_DIR"],
}),
);
expect(check?.detail).toContain("It records other values for JAVA_HOME.");
expect(check?.detail).toContain(
"REA could not resolve its GHIDRA_INSTALL_DIR.",
);
expect(check?.detail).not.toContain("does not record");
});
-12
View File
@@ -1,4 +1,3 @@
import { resolveInvestigationEvidenceClosure } from "./investigation/InvestigationEvidence.js";
import { analysisEvidenceSources } from "./AnalysisEvidenceSources.js";
import { dispatchQueuedInterrupts } from "../process/queuedInterrupts.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
@@ -181,17 +180,6 @@ export const publishEvidenceDelivery = async (input: {
readonly signal?: AbortSignal | undefined;
}): Promise<Result<PublishedEvidenceDelivery, AnalysisError>> => {
const { signal, operation } = input;
// Partial analysis can hold only some of its linked sources. Both delivery
// adapters must establish closure before committing a complete source bundle.
const sources = new Map(
input.bundle.records.map((record) => [record.evidence_id, record]),
);
const closure = resolveInvestigationEvidenceClosure(
{ evidenceForAnalysis: (id) => sources.get(id) },
[input.complete.evidence_id],
);
if (!closure.ok)
return err(closure.error.retainPartialObservation(input.complete));
const written = await persistEvidenceBundleCommand(
input.bundle,
input.outputPath,
@@ -18,10 +18,7 @@ import {
FINALIZED_PROCESS_CAPTURE_EXAMPLE,
} from "../domain/process/processCapture.fixture.js";
import { PROCESS_PROVIDER } from "../domain/process/processEvidenceProvider.js";
import {
parseProcessCapture,
safeParseProcessCapture,
} from "../domain/process/processCaptureParsing.js";
import { parseProcessCapture } from "../domain/process/processCaptureParsing.js";
import {
digestProcessCommitment,
processScenarioSchema,
@@ -255,10 +252,6 @@ describe("reconstruction obligation candidates", () => {
},
},
});
const invalid = safeParseProcessCapture(evidence.normalized_result);
expect(invalid.success).toBe(false);
if (invalid.success) throw new Error("Expected invalid capture fixture");
expect(invalid.error.issues).toHaveLength(2);
const generated = deriveCandidates(createEvidenceBundle([evidence]), []);
expect(generated.limitations).toHaveLength(1);
@@ -266,7 +259,6 @@ describe("reconstruction obligation candidates", () => {
expect(generated.limitations[0]).toMatch(
/Process Evidence.*could not generate candidates.*manifest\.completed_at/iu,
);
expect(generated.limitations[0]).toContain("(1 more issues)");
});
});
@@ -9,7 +9,6 @@ import {
proofEvidence,
request,
reviewedObligation,
readyReviewedLedgerRequest,
} from "./ReconstructionObligationLedger.fixture.js";
import { createEvidence } from "../domain/evidence.js";
import type { ReviewedReconstructionObligation } from "../domain/reconstructionObligationLedgerSchemas.js";
@@ -283,23 +282,7 @@ describe("reconstruction obligation ledger", () => {
});
describe("reconstruction obligation ledger fail-closed behavior", () => {
it("requires a parser, schema and domain type for a reviewed parser boundary", () => {
const input = readyReviewedLedgerRequest();
input.reviewed_obligations[0]!.requires_parser_type = true;
const missing = build(input);
expect(missing.status).toBe("open");
expect(missing.obligations[0]?.diagnostics.map(({ code }) => code)).toEqual(
["missing-parser-type"],
);
input.manifest.bindings[0]!.parser_type = {
parser: "parseRequest",
schema: "requestSchema",
domain_type: "Request",
};
expect(build(input).status).toBe("ready");
});
it("fails closed on ambiguous ownership and contradiction", () => {
it("fails closed on parser gaps, ambiguous ownership, and contradiction", () => {
const original = createEvidence(
undefined,
{ id: "external", name: "External protocol observer", version: "1" },
@@ -37,6 +37,7 @@ import {
} from "./ReconstructionObligationOriginalCases.js";
import { obligationLedgerEvidenceLinks } from "./ReconstructionObligationLedgerProjection.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
import { appendAll } from "../domain/arrayAppend.js";
type Diagnostic = ReconstructionObligation["diagnostics"][number];
type Binding = NonNullable<ReconstructionObligation["binding"]>;
@@ -210,13 +211,15 @@ const evaluateObligation = (
);
if (binding !== null)
evaluateBinding(candidate, binding, context, diagnostics);
diagnostics.push(
...originalCaseDiagnostics(candidate, originalCases, context.evidence),
appendAll(
diagnostics,
originalCaseDiagnostics(candidate, originalCases, context.evidence),
);
evaluateAuthority(candidate, originalCases, context, diagnostics);
evaluateDisposition(candidate, diagnostics);
diagnostics.push(
...contradictionDiagnostics(
appendAll(
diagnostics,
contradictionDiagnostics(
candidate.obligation_id,
context.contradictions,
context.evidence,
@@ -24,6 +24,7 @@ import type {
ReferenceSourceRead,
} from "../reference/ReferenceSourceReaderTypes.js";
import { typescriptEmittedSourceCandidates } from "./TypeScriptEmittedSourceCandidates.js";
import { appendAll } from "../domain/arrayAppend.js";
export interface ParsedReferenceSourceEntries {
readonly entries: HistoricalSourceGraphInput["entries"];
@@ -473,8 +474,9 @@ const resolveInternalSpecifier = (
// Retain present runtime files first; substitute only a missing counterpart.
// Counterparts cover explicit filenames, not directory/index resolution.
if (!directoryReference)
candidates.push(
...typescriptEmittedSourceCandidates(fromPath, normalized).map(
appendAll(
candidates,
typescriptEmittedSourceCandidates(fromPath, normalized).map(
({ path }) => path,
),
);
@@ -564,7 +566,7 @@ const appendFile = (
limitations: [...(parsed?.limitations ?? [])],
});
if (parsed === undefined) return;
output.limitations.push(...parsed.limitations);
appendAll(output.limitations, parsed.limitations);
for (const relationship of parsed.relationships) {
const { resolutionStrategy, resolutionBasePath, ...graphRelationship } =
relationship;
@@ -598,7 +600,7 @@ const appendFile = (
resolution: resolved.resolution,
});
}
output.parseFailures.push(...parsed.parse_failures);
appendAll(output.parseFailures, parsed.parse_failures);
};
const appendReadEntry = (
+1 -1
View File
@@ -481,7 +481,7 @@ describe("setup plan application", () => {
expect(host.doctorCalls).toBe(1);
});
it("completes an empty interactive selection without changes", async () => {
it("treats an empty interactive selection as cancellation", async () => {
const host = new FakeSetupHost();
host.clients = [{ name: "codex", configPath: "/codex.toml" }];
@@ -213,6 +213,8 @@ it.each(["pending", "already recovered"] as const)(
expect(source.cleanupIncomplete).toBe(false);
expect(first?.cleanupIncomplete).toBe(true);
expect(second?.cleanupIncomplete).toBe(true);
expect(first?.cleanupResources).toContain("/delegated");
expect(second?.cleanupResources).toContain("/delegated");
expect(first?.cleanupResources).toEqual(["/delegated"]);
expect(second?.cleanupResources).toEqual(["/delegated"]);
expect(await owner.close()).toBeUndefined();
+4 -2
View File
@@ -71,6 +71,7 @@ import {
registrationWriteBlocker,
type SetupClient,
} from "./SupportedClients.js";
import { appendAll } from "../domain/arrayAppend.js";
export class ClientConfigurationCleanupRollbackFailure extends RegularFileCleanupFailure<string> {
constructor(
@@ -729,8 +730,9 @@ const registrationOwnershipBlocker = (
value: parsed.legacyServers[name],
paths: [client.configPath],
});
entries.push(
...shadowedClientServers(original, parsed, name).legacyServers.map(
appendAll(
entries,
shadowedClientServers(original, parsed, name).legacyServers.map(
(value) => ({ value, paths: [client.configPath] }),
),
);
+7 -5
View File
@@ -24,6 +24,7 @@ import {
type SkillFileSnapshot,
type SkillPublicationCleanupOwner,
} from "./SkillFilePublication.js";
import { appendAll } from "../domain/arrayAppend.js";
export const MANAGED_SKILL_FILES = [
"SKILL.md",
@@ -281,7 +282,7 @@ export const existingSkillDestinations = async (
try {
const pathIssues = await skillDirectoryIssues([destination]);
if (pathIssues.some(({ kind }) => kind === "unreadable")) {
issues.push(...pathIssues);
appendAll(issues, pathIssues);
continue;
}
if (pathIssues.length > 0) continue;
@@ -487,7 +488,7 @@ const inspectCanonicalSkill = async (
const { path: root } = destination;
const pathIssues = await skillDirectoryIssues([destination]);
if (pathIssues.length > 0) {
issues.push(...pathIssues);
appendAll(issues, pathIssues);
if (failureCause === undefined) failureCause = pathIssues;
installedDestination = true;
canonical = false;
@@ -530,7 +531,7 @@ const inspectCanonicalSkill = async (
if (clientIds !== undefined || rootHasFiles || rootIssues.length > 0) {
installedDestination ||= rootHasFiles;
canonical = canonical && rootCanonical;
issues.push(...rootIssues);
appendAll(issues, rootIssues);
if (!rootCanonical && rootIssues.length === 0)
issues.push({
client: destination.client,
@@ -704,8 +705,9 @@ export const installCanonicalSkill = async (
}
}
const cleanupOwners = [...publications, ...preparedCleanupOwners];
rollbackFailures.push(
...(await closeSkillPublications(cleanupOwners, resources)),
appendAll(
rollbackFailures,
await closeSkillPublications(cleanupOwners, resources),
);
if (verified && rollbackFailures.length === 0) return ok("installed");
return err(
+18 -9
View File
@@ -69,6 +69,7 @@ import {
redirectedWriteBlocker,
type MsixPackageView,
} from "./MsixPackageStorage.js";
import { appendAll } from "../domain/arrayAppend.js";
interface ManagedPathStats {
readonly uid?: number;
@@ -255,7 +256,7 @@ const removeAll = async (
];
}
items.push(await host.removeSkill());
if (purgeData) items.push(...(await host.purgeData()));
if (purgeData) appendAll(items, await host.purgeData());
return items;
};
@@ -1223,17 +1224,24 @@ const purgeSelectedData = async (
const owned = names.filter(
(name) => !kept.some((entry) => entry.name === name),
);
return async () => [
...keptItems,
...(await purgeManagedData(root, fileSystem, owned)),
];
return async () => {
const removed = await purgeManagedData(root, fileSystem, owned);
const result: UninstallItem[] = [];
appendAll(result, keptItems);
appendAll(result, removed);
return result;
};
};
const reaPlan = await purge(reaRoot, ["cache", "state"]);
const plans: (() => Promise<readonly UninstallItem[]>)[] = [];
for (const directory of directories)
plans.push(await purge(dirname(directory), [basename(directory)]));
items.unshift(...(await reaPlan()));
for (const plan of plans) items.push(...(await plan()));
const merged: UninstallItem[] = [];
appendAll(merged, await reaPlan());
appendAll(merged, items);
for (const plan of plans) appendAll(merged, await plan());
items.length = 0;
appendAll(items, merged);
return items;
};
@@ -1605,8 +1613,9 @@ const collapseUninstallItems = (
for (const next of items) {
const { name, actions, ...action } = next;
const group = grouped.get(name) ?? [];
group.push(
...(actions !== undefined && actions.length > 0 ? actions : [action]),
appendAll(
group,
actions !== undefined && actions.length > 0 ? actions : [action],
);
grouped.set(name, group);
}
@@ -40,7 +40,7 @@ it.each([
});
},
);
it("rejects source text changed across the artifact port before acquiring the resolver", async () => {
it("rejects source text changed across the artifact port while retaining the original digest", async () => {
const artifacts = {
...webModuleArtifactsFixture("import(name)"),
source: "import(other)",
@@ -304,6 +304,22 @@ it("honors cancellation both before acquisition and across decoder completion",
].map((path) => ({ kind: "artifact-path", path })),
});
const malformedDecoder: WebSourceMapPort = {
trace: async (input, options) => {
const decoded = await decoder.trace(input, options);
return decoded.ok
? ok({ ...decoded.value, source_map_sha256: "b".repeat(64) })
: decoded;
},
};
const malformed = await new WebSourceLocationService(
{ load: () => Promise.resolve(ok(webSourceLocationFixture())) },
malformedDecoder,
).trace(args);
expect(malformed.ok).toBe(false);
if (!malformed.ok)
expect(malformed.error).toBeInstanceOf(AnalysisOutputError);
const invalidController = new AbortController();
const malformedLate = await new WebSourceLocationService(
{ load: () => Promise.resolve(ok(webSourceLocationFixture())) },
+34
View File
@@ -725,6 +725,40 @@ describe("workflow residual question retention", () => {
);
});
it.each([
["spaces", " Which boundary? "],
["line feed and tab", "\nWhich boundary?\t"],
["byte order mark", "\uFEFFWhich boundary?\uFEFF"],
])(
"normalizes edge whitespace from %s without changing workflow Evidence",
(_, question) => {
const records = new InvestigationRecords();
const result = { residual_unknowns: [question] };
const evidence = createWorkflowEvidence({
target: undefined,
operation: "inspect_native_api",
parameters: {},
result,
upstreamProfile: undefined,
upstreamLimitations: [],
});
expect(records.recordEvidence(evidence).ok).toBe(true);
expect(
recordWorkflowUnknowns({
name: "inspect_native_api",
result,
evidenceId: evidence.evidence_id,
recordUnknown: (unknown) => records.recordUnknown(unknown),
}).ok,
).toBe(true);
expect(records.listUnknowns()).toMatchObject([
{ question: `Which boundary? (Evidence ${evidence.evidence_id})` },
]);
expect(evidence.normalized_result).toEqual(result);
},
);
it("rejects questions that are empty after whitespace normalization", () => {
const records = new InvestigationRecords();
const result = { residual_unknowns: [" \n\t "] };
@@ -36,7 +36,7 @@ import {
isSourceEvidenceRetainable,
} from "./AnalysisSnapshotCache.js";
import { EvidenceLedger } from "../investigation/EvidenceLedger.js";
import { nativeCallGraphSchema } from "../../domain/native/nativeCallGraph.js";
import { enhancedOutputSchemas } from "../../contracts/toolOutputSchemaGroups.js";
it.each(["provider", "workflow"] as const)(
"owns nested %s execution data before returning control to callers",
@@ -1270,13 +1270,12 @@ it.each([
evidenceProfile: revisedProfile,
}),
).toBeUndefined();
if (operation === "get_call_graph") {
expect(nativeCallGraphSchema.safeParse(oldResult).success).toBe(false);
if (operation === "get_call_graph")
expect(
nativeCallGraphSchema.safeParse({ graph: oldResult, limitations: [] })
.success,
).toBe(true);
}
enhancedOutputSchemas.get_call_graph.safeParse({
normalized_result: oldResult,
}).success,
).toBe(false);
},
);
@@ -18,8 +18,6 @@ const identity: ProviderIdentity = {
it("retains both child close failures when a resource owner rejects", async () => {
const attempts: string[] = [];
const artifactCause = new Error("artifact close rejected");
const nativeCause = new Error("native close rejected");
const artifact: AnalysisClient = {
execute: async () => ({
ok: true,
@@ -27,7 +25,7 @@ it("retains both child close failures when a resource owner rejects", async () =
}),
close: async () => {
attempts.push("artifact");
throw artifactCause;
throw new Error("artifact close rejected");
},
};
const native: AnalysisClient = {
@@ -37,20 +35,14 @@ it("retains both child close failures when a resource owner rejects", async () =
}),
close: async () => {
attempts.push("native");
throw nativeCause;
throw new Error("native close rejected");
},
};
const closed = await combineBundleResourceClients(artifact, native).close();
expect(attempts.sort()).toEqual(["artifact", "native"]);
expect(closed.ok).toBe(false);
if (closed.ok) throw new Error("Expected both child close failures");
expect(closed.error.cleanupIncomplete).toBe(true);
expect(closed.error.cause).toBeInstanceOf(AggregateError);
const failures = (closed.error.cause as AggregateError).errors;
expect(failures).toHaveLength(2);
expect((failures[0] as Error).cause).toBe(artifactCause);
expect((failures[1] as Error).cause).toBe(nativeCause);
if (!closed.ok) expect(closed.error.cleanupIncomplete).toBe(true);
});
it("reports the first mixed child failure before awaiting the other owner and retries both", async () => {
@@ -135,7 +135,6 @@ const TARGETS: TargetFixtures = {
espFlash: { selected: true, flashOffset: null, partitionTableOffset: null },
},
fit: { ...archive, format: "fit" },
"ntfs-mft": { ...archive, format: "ntfs-mft" },
plist: { ...artifact, format: "plist" },
javascript: { ...artifact, format: "javascript" },
"source-map": { ...artifact, format: "source-map" },
@@ -1,4 +1,3 @@
import { parseEvidence } from "../../domain/evidence.js";
import { expect, it } from "vitest";
import { AppleIpsService } from "./AppleIpsService.js";
import { APPLE_IPS_PROVIDER } from "../../domain/investigationProviders.js";
@@ -233,13 +232,12 @@ it.each([
},
);
it("retains a completed observation when the caller cancels as the provider completes", async () => {
it("discards an observation if the caller cancels while the provider completes", async () => {
const controller = new AbortController();
const reason = { request: "late AppleIps cancellation" };
const service = new AppleIpsService({
identity,
inspect: () => {
controller.abort(reason);
controller.abort();
return Promise.resolve(ok(report));
},
});
@@ -247,13 +245,8 @@ it("retains a completed observation when the caller cancels as the provider comp
{ path: report.artifact.path },
{ signal: controller.signal },
);
if (result.ok) throw new Error("Expected late cancellation");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
expect(parseEvidence(result.error.partialObservation)).toMatchObject({
operation: "inspect_apple_ips",
normalized_result: report,
});
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
});
it.each([
@@ -1,4 +1,3 @@
import { parseEvidence } from "../../domain/evidence.js";
import { expect, it } from "vitest";
import { DevicetreeService } from "./DevicetreeService.js";
import { DEVICETREE_PROVIDER } from "../../domain/investigationProviders.js";
@@ -73,13 +72,12 @@ it("validates caller paths before invoking the provider", async () => {
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisInputError);
expect(calls).toBe(0);
});
it("retains a completed observation when the caller cancels as the provider completes", async () => {
it("rejects cancellation at provider completion", async () => {
const controller = new AbortController();
const reason = { request: "late Devicetree cancellation" };
const service = new DevicetreeService({
identity: DEVICETREE_PROVIDER,
inspect: () => {
controller.abort(reason);
controller.abort();
return Promise.resolve(ok(report()));
},
});
@@ -87,13 +85,8 @@ it("retains a completed observation when the caller cancels as the provider comp
{ path: "/board.dtb" },
{ signal: controller.signal },
);
if (result.ok) throw new Error("Expected late cancellation");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
expect(parseEvidence(result.error.partialObservation)).toMatchObject({
operation: "inspect_devicetree",
normalized_result: report(),
});
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
});
it("rejects provider coverage and source-range inconsistencies", async () => {
for (const change of [
@@ -1,4 +1,3 @@
import { parseEvidence } from "../../domain/evidence.js";
import { expect, it } from "vitest";
import { ElfNotesService } from "./ElfNotesService.js";
import { ELF_NOTES_PROVIDER } from "../../domain/investigationProviders.js";
@@ -107,13 +106,12 @@ it("retains an examined empty table and rejects an uncounted table", async () =>
}
});
it("retains a completed observation when the caller cancels as the provider completes", async () => {
it("discards an observation if the caller cancels while the provider completes", async () => {
const controller = new AbortController();
const reason = { request: "late ElfNotes cancellation" };
const service = new ElfNotesService({
identity,
inspect: () => {
controller.abort(reason);
controller.abort();
return Promise.resolve(ok(report));
},
});
@@ -121,11 +119,6 @@ it("retains a completed observation when the caller cancels as the provider comp
{ path: report.artifact.path },
{ signal: controller.signal },
);
if (result.ok) throw new Error("Expected late cancellation");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
expect(parseEvidence(result.error.partialObservation)).toMatchObject({
operation: "inspect_elf_notes",
normalized_result: report,
});
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
});
@@ -1,4 +1,3 @@
import { parseEvidence } from "../../domain/evidence.js";
import { expect, it } from "vitest";
import { ElfUnwindRangesService } from "./ElfUnwindRangesService.js";
import { ELF_UNWIND_RANGES_PROVIDER } from "../../domain/investigationProviders.js";
@@ -407,13 +406,12 @@ it("rejects a report that fails the output schema", async () => {
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisOutputError);
});
it("retains a completed observation when the caller cancels as the provider completes", async () => {
it("discards an observation if the caller cancels while the provider completes", async () => {
const controller = new AbortController();
const reason = { request: "late ElfUnwindRanges cancellation" };
const service = new ElfUnwindRangesService({
identity,
inspect: () => {
controller.abort(reason);
controller.abort();
return Promise.resolve(ok(report));
},
});
@@ -421,11 +419,6 @@ it("retains a completed observation when the caller cancels as the provider comp
{ path: report.artifact.path },
{ signal: controller.signal },
);
if (result.ok) throw new Error("Expected late cancellation");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
expect(parseEvidence(result.error.partialObservation)).toMatchObject({
operation: "inspect_elf_unwind_ranges",
normalized_result: report,
});
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
});
@@ -1,4 +1,3 @@
import { parseEvidence } from "../../domain/evidence.js";
import { expect, it } from "vitest";
import { NativeInitializersService } from "./NativeInitializersService.js";
import { NATIVE_INITIALIZERS_PROVIDER } from "../../domain/investigationProviders.js";
@@ -114,13 +113,12 @@ it("rejects a slot offset outside the artifact", async () => {
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisOutputError);
});
it("retains a completed observation when the caller cancels as the provider completes", async () => {
it("discards an observation if the caller cancels while the provider completes", async () => {
const controller = new AbortController();
const reason = { request: "late NativeInitializers cancellation" };
const service = new NativeInitializersService({
identity,
inspect: () => {
controller.abort(reason);
controller.abort();
return Promise.resolve(ok(report));
},
});
@@ -128,11 +126,6 @@ it("retains a completed observation when the caller cancels as the provider comp
{ path: report.artifact.path },
{ signal: controller.signal },
);
if (result.ok) throw new Error("Expected late cancellation");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
expect(parseEvidence(result.error.partialObservation)).toMatchObject({
operation: "inspect_native_initializers",
normalized_result: report,
});
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
});
@@ -1,4 +1,3 @@
import { parseEvidence } from "../../domain/evidence.js";
import { createHash } from "node:crypto";
import { inspectPeCertificatesInputSchema } from "../../domain/native/peCertificates.js";
import { peCertificateFixture } from "../../native/pe/PeCertificates.fixture.js";
@@ -50,13 +49,12 @@ it("rejects a relative path and extra arguments before reading", async () => {
expect(calls).toBe(0);
});
it("retains a completed observation when the caller cancels as the provider completes", async () => {
it("discards an observation if the caller cancels while the provider completes", async () => {
const controller = new AbortController();
const reason = { request: "late PeCertificates cancellation" };
const service = new PeCertificatesService({
identity,
inspect: () => {
controller.abort(reason);
controller.abort();
return Promise.resolve(ok(report));
},
});
@@ -64,13 +62,8 @@ it("retains a completed observation when the caller cancels as the provider comp
{ path: report.artifact.path },
{ signal: controller.signal },
);
if (result.ok) throw new Error("Expected late cancellation");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
expect(parseEvidence(result.error.partialObservation)).toMatchObject({
operation: "inspect_pe_certificates",
normalized_result: report,
});
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
});
it("retains complete certificate provenance from the production parser", async () => {
@@ -1,4 +1,3 @@
import { parseEvidence } from "../../domain/evidence.js";
import { expect, it } from "vitest";
import { PeImportsService } from "./PeImportsService.js";
import { PE_IMPORTS_PROVIDER } from "../../domain/investigationProviders.js";
@@ -117,13 +116,12 @@ it.each(["import_directory", "delay_import_directory"] as const)(
},
);
it("retains a completed observation when the caller cancels as the provider completes", async () => {
it("discards an observation if the caller cancels while the provider completes", async () => {
const controller = new AbortController();
const reason = { request: "late PeImports cancellation" };
const service = new PeImportsService({
identity,
inspect: () => {
controller.abort(reason);
controller.abort();
return Promise.resolve(ok(report));
},
});
@@ -131,13 +129,8 @@ it("retains a completed observation when the caller cancels as the provider comp
{ path: report.artifact.path },
{ signal: controller.signal },
);
if (result.ok) throw new Error("Expected late cancellation");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
expect(parseEvidence(result.error.partialObservation)).toMatchObject({
operation: "inspect_pe_imports",
normalized_result: report,
});
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
});
it("publishes every consumed directory span as its own Evidence location", async () => {
@@ -1,4 +1,3 @@
import { parseEvidence } from "../../domain/evidence.js";
import { expect, it } from "vitest";
import { WindowsShortcutService } from "./WindowsShortcutService.js";
import { WINDOWS_SHORTCUT_PROVIDER } from "../../domain/investigationProviders.js";
@@ -144,13 +143,12 @@ it("rejects complete coverage that still lists issues", async () => {
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisOutputError);
});
it("retains a completed observation when the caller cancels as the provider completes", async () => {
it("discards an observation if the caller cancels while the provider completes", async () => {
const controller = new AbortController();
const reason = { request: "late WindowsShortcut cancellation" };
const service = new WindowsShortcutService({
identity,
inspect: () => {
controller.abort(reason);
controller.abort();
return Promise.resolve(ok(report));
},
});
@@ -158,11 +156,6 @@ it("retains a completed observation when the caller cancels as the provider comp
{ path: report.artifact.path },
{ signal: controller.signal },
);
if (result.ok) throw new Error("Expected late cancellation");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
expect(parseEvidence(result.error.partialObservation)).toMatchObject({
operation: "inspect_windows_shortcut",
normalized_result: report,
});
expect(result.ok).toBe(false);
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
});
Loaded 100 of 537 files, more files were not shown because too many files have changed in this diff. Show more