mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 17:30:32 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5fb8939a28 | ||
|
|
f366d21446 | ||
|
|
2885347cde | ||
|
|
a6a2527ade | ||
|
|
f8fc572e4a | ||
|
|
ab9e5fa204 | ||
|
|
8d45cb45c4 | ||
|
|
7712e3185e | ||
|
|
466149169e | ||
|
|
1c2626edbd | ||
|
|
b16d18f198 | ||
|
|
14030e2f11 | ||
|
|
9505c73824 | ||
|
|
1a446cdade | ||
|
|
1ef7f02cae | ||
|
|
7b86474be7 | ||
|
|
5d10337709 | ||
|
|
8eadb872f2 | ||
|
|
f170175ec8 | ||
|
|
fbe09f84df | ||
|
|
ebb480ba22 | ||
|
|
da1f029c2f | ||
|
|
4db9c166ef | ||
|
|
29fd6a7d03 | ||
|
|
d7338876d6 | ||
|
|
9df3f7174f | ||
|
|
79e476204f | ||
|
|
9c95218374 | ||
|
|
d9d2641981 | ||
|
|
881aec416a | ||
|
|
f8d17481e8 | ||
|
|
e2585aa7ce | ||
|
|
1d1c7f98ff | ||
|
|
b1f1b3eff3 | ||
|
|
c1d0f2bb45 | ||
|
|
04de5f3200 | ||
|
|
fe791ff159 | ||
|
|
199b006cc3 | ||
|
|
db1db1a965 | ||
|
|
bf7f518e2d | ||
|
|
4082375e73 | ||
|
|
dcee59f273 | ||
|
|
31018cf037 | ||
|
|
17f0c6944c | ||
|
|
360cd6f438 | ||
|
|
b129d39ac9 | ||
|
|
fbb785293b | ||
|
|
1b8a7ee7f6 | ||
|
|
86b1b1431a | ||
|
|
d5e2d59eb6 | ||
|
|
03e43c0f1b | ||
|
|
724211b468 | ||
|
|
fe1a5b1498 | ||
|
|
fc3f18b748 | ||
|
|
0a66fb2acf | ||
|
|
56d2ba36b8 | ||
|
|
0ea642843a | ||
|
|
0c37c22d51 | ||
|
|
b32da4aefa | ||
|
|
c8a52fd478 | ||
|
|
67f402fb81 | ||
|
|
1ef28be5bc | ||
|
|
de0c50ead7 | ||
|
|
fe4105187d | ||
|
|
b05f472592 | ||
|
|
38286d4bc3 | ||
|
|
15e3a73ba6 | ||
|
|
38dc9d246b | ||
|
|
6736b78693 | ||
|
|
b179755e4f | ||
|
|
42f61205c3 | ||
|
|
b4c8799123 | ||
|
|
fd584d4495 | ||
|
|
3c4be703cd | ||
|
|
439c197c3f | ||
|
|
a8e815d664 | ||
|
|
c1e3d7dc6e | ||
|
|
8661bb38d5 | ||
|
|
0ec73a8102 | ||
|
|
93dc8d044d | ||
|
|
6377a1831f | ||
|
|
a9882a58b2 | ||
|
|
25f50eea02 | ||
|
|
b899c35419 | ||
|
|
a4b4957346 | ||
|
|
ddb8abf855 | ||
|
|
99e79026c5 | ||
|
|
b4c3a028fe | ||
|
|
5e4055b39e | ||
|
|
5b34c4d3f5 | ||
|
|
db6400d775 | ||
|
|
dc7594e85f | ||
|
|
3345019fe1 | ||
|
|
9dfec80e93 | ||
|
|
005435ca78 | ||
|
|
dbfab4a65d | ||
|
|
b2f3dc81fa | ||
|
|
a4da4e7230 | ||
|
|
433fd9486d |
No files matched your search
@@ -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 bytes and never
|
||||
# The bridge probe imports only source-owned ARM, Thumb and MIPS 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 and Thumb instruction context through the bridge
|
||||
- name: Inspect real ARM, Thumb and MIPS instruction context through the bridge
|
||||
shell: bash
|
||||
run: |
|
||||
java_home_variable="JAVA_HOME_${GHIDRA_JAVA_MAJOR}_X64"
|
||||
@@ -58,10 +58,21 @@ 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 ReaArmContextProbe.java "${GITHUB_WORKSPACE}/bridge/ghidra" \
|
||||
-postScript ReaInstructionContextProbe.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
|
||||
|
||||
@@ -76,14 +76,12 @@
|
||||
{
|
||||
"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",
|
||||
|
||||
@@ -619,6 +619,10 @@ 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
|
||||
|
||||
@@ -187,15 +187,6 @@ 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
|
||||
|
||||
@@ -93,6 +93,38 @@ 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
|
||||
|
||||
@@ -1413,6 +1413,28 @@ 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,6 +317,9 @@ 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
|
||||
|
||||
@@ -32,9 +32,14 @@ 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. 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. 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.
|
||||
|
||||
`config` is `present` only when all of the following hold:
|
||||
|
||||
|
||||
@@ -62,7 +62,10 @@ 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. This workflow does not emit verdicts
|
||||
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
|
||||
about intent.
|
||||
|
||||
A missing optional section is observed absence of that structure. It is not
|
||||
|
||||
@@ -60,6 +60,48 @@ 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
|
||||
@@ -576,13 +618,23 @@ batch-workload limit tracked in issue #5930.
|
||||
|
||||
### Error recovery and deadline ownership
|
||||
|
||||
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
|
||||
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
|
||||
(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
|
||||
|
||||
@@ -26,6 +26,7 @@ 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,7 +26,10 @@ 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. The scan is
|
||||
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
|
||||
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`,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# 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
|
||||
@@ -0,0 +1,51 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,11 @@
|
||||
# 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.
|
||||
@@ -11,6 +11,9 @@
|
||||
"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",
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
"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",
|
||||
|
||||
@@ -247,6 +247,8 @@ 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",
|
||||
@@ -493,6 +495,8 @@ 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",
|
||||
@@ -632,7 +636,7 @@ const PATH_SCOPES = {
|
||||
"src/ghidra/GhidraJavaProperties.ts",
|
||||
"src/ghidra/GhidraReleasePolicy.ts",
|
||||
"scripts/verify-ghidra-bridge-sources.mjs",
|
||||
"tests/conformance/ghidra/ReaArmContextProbe.java",
|
||||
"tests/conformance/ghidra/ReaInstructionContextProbe.java",
|
||||
"tests/conformance/ghidra/ReaComputedCallCoverageProbe.java",
|
||||
"tests/conformance/ghidra/ReaDecodeProbe.java",
|
||||
".github/workflows/ghidra-bridge-sources.yml",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
/** 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>;
|
||||
@@ -0,0 +1,24 @@
|
||||
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,11 +375,15 @@ 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("/usr/bin/sleep", ["60"], {
|
||||
detached: true,
|
||||
env: { ...process.env, REA_PROCESS_RUN_ID: runId },
|
||||
stdio: "ignore",
|
||||
});
|
||||
const child = spawn(
|
||||
process.execPath,
|
||||
["--eval", "setInterval(() => {}, 1000)"],
|
||||
{
|
||||
detached: true,
|
||||
env: { ...process.env, REA_PROCESS_RUN_ID: runId },
|
||||
stdio: "ignore",
|
||||
},
|
||||
);
|
||||
let primaryError;
|
||||
let cleaned = false;
|
||||
try {
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface RealHopperFixtureTargets {
|
||||
readonly manifestPath: string;
|
||||
readonly primary: RealHopperFixtureTarget;
|
||||
readonly secondary: RealHopperFixtureTarget;
|
||||
readonly unicode?: RealHopperFixtureTarget;
|
||||
readonly large: RealHopperFixtureTarget;
|
||||
readonly oracle: RealHopperFixtureOracle;
|
||||
readonly largeOracle: RealHopperLargeFixtureOracle;
|
||||
|
||||
@@ -34,9 +34,8 @@ 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),
|
||||
]:
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createVerifierMcpOwner } from "./verifier-mcp-owner.mjs";
|
||||
import { withVerifierCleanup } from "./verifier-cleanup.mjs";
|
||||
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";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
|
||||
/** Install the selected archive or registry specifier before any protocol deadline. */
|
||||
export async function installCanaryPackage(specifier, workspace, environment) {
|
||||
/** Install the selected archive or registry specifier with an owned ten-minute deadline. */
|
||||
export async function installCanaryPackage(
|
||||
specifier,
|
||||
workspace,
|
||||
environment,
|
||||
commands,
|
||||
) {
|
||||
const prefix = join(workspace, "installed");
|
||||
await execute(
|
||||
await commands.exec(
|
||||
"npm",
|
||||
[
|
||||
"install",
|
||||
@@ -26,7 +29,12 @@ export async function installCanaryPackage(specifier, workspace, environment) {
|
||||
prefix,
|
||||
specifier,
|
||||
],
|
||||
{ cwd: workspace, env: environment, maxBuffer: 16 * 1024 * 1024 },
|
||||
{
|
||||
cwd: workspace,
|
||||
env: environment,
|
||||
timeout: 600000,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
return join(prefix, "node_modules", "rea-agents");
|
||||
}
|
||||
@@ -36,7 +44,7 @@ export async function verifyInstalledCanaryPackage(
|
||||
packageRoot,
|
||||
version,
|
||||
workspace,
|
||||
environment,
|
||||
{ environment, commands },
|
||||
) {
|
||||
const installed = JSON.parse(
|
||||
await readFile(join(packageRoot, "package.json"), "utf8"),
|
||||
@@ -47,9 +55,10 @@ export async function verifyInstalledCanaryPackage(
|
||||
);
|
||||
const launcher = join(packageRoot, "scripts", "rea.mjs");
|
||||
for (const option of ["--help", "--llms"])
|
||||
await execute(process.execPath, [launcher, option], {
|
||||
await commands.exec(process.execPath, [launcher, option], {
|
||||
cwd: workspace,
|
||||
env: environment,
|
||||
timeout: 30000,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
});
|
||||
const transport = new StdioClientTransport({
|
||||
@@ -65,22 +74,20 @@ export async function verifyInstalledCanaryPackage(
|
||||
if (stderr.length < 16_384)
|
||||
stderr += chunk.toString("utf8").slice(0, 16_384 - stderr.length);
|
||||
});
|
||||
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 {
|
||||
const owner = createVerifierMcpOwner();
|
||||
commands.adopt(owner, "installed package MCP transport");
|
||||
return withVerifierCleanup(async () => {
|
||||
try {
|
||||
await client.close();
|
||||
} finally {
|
||||
await transport.close();
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [() => owner.close()]);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/** 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;
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
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";
|
||||
@@ -12,7 +11,6 @@ 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,
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import type { RuleTester } from "oxlint/plugins-dev";
|
||||
declare const rule: Parameters<RuleTester["run"]>[1];
|
||||
export default rule;
|
||||
@@ -1,57 +0,0 @@
|
||||
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 },
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -46,6 +46,7 @@ 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",
|
||||
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
@@ -13,6 +14,19 @@ 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,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { closeAgentEvaluation } from "./lib/agent-evaluation-cleanup.mjs";
|
||||
import { importVerifierSource } from "./lib/verifier-source-loader.mjs";
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
@@ -18,10 +18,7 @@ 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 {
|
||||
closeVerifierResources,
|
||||
withVerifierCleanup,
|
||||
} from "./lib/verifier-cleanup.mjs";
|
||||
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
|
||||
const { parseSkillManifestMetadata } = await importVerifierSource(
|
||||
@@ -427,16 +424,13 @@ await withVerifierCleanup(
|
||||
},
|
||||
// Even a deliberately retained fixture directory must never retain auth.
|
||||
[
|
||||
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 })]),
|
||||
]);
|
||||
},
|
||||
() =>
|
||||
closeAgentEvaluation({
|
||||
commands,
|
||||
authPath,
|
||||
evaluationRoot,
|
||||
keepFixtures: process.env.REA_AGENT_EVAL_KEEP_FIXTURES === "true",
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ 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,
|
||||
@@ -79,6 +80,32 @@ 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`,
|
||||
);
|
||||
|
||||
@@ -165,7 +165,9 @@ try {
|
||||
scenario.value.steps.some((step) => step.outcome !== "completed") ||
|
||||
!scenario.value.steps
|
||||
.at(-1)
|
||||
?.after?.nodes.some((node) => node.title === "REA fixture incremented")
|
||||
?.after?.nodes.some(
|
||||
(node) => node.title === "REA fixture incremented 1",
|
||||
)
|
||||
)
|
||||
throw new Error(
|
||||
`Source-owned AX scenario failed: ${JSON.stringify(scenario.value.steps.map(({ outcome, reason }) => ({ outcome, reason })))}`,
|
||||
@@ -195,7 +197,12 @@ try {
|
||||
evidence.normalized_result?.steps?.some(
|
||||
(step) => step.outcome !== "completed",
|
||||
) ||
|
||||
evidence.normalized_result?.steps?.length !== 2
|
||||
evidence.normalized_result?.steps?.length !== 2 ||
|
||||
!evidence.normalized_result?.steps
|
||||
?.at(-1)
|
||||
?.after?.nodes.some(
|
||||
(node) => node.title === "REA fixture incremented 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 })))}`,
|
||||
@@ -232,7 +239,7 @@ try {
|
||||
!captured.steps
|
||||
.at(-1)
|
||||
?.after?.nodes.some(
|
||||
(node) => node.title === "REA fixture incremented",
|
||||
(node) => node.title === "REA fixture incremented 3",
|
||||
)
|
||||
)
|
||||
throw new Error("MCP did not observe the selected fixture's AX action");
|
||||
|
||||
@@ -191,6 +191,8 @@ 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",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { createPackageVerifierCommands } from "./lib/verify-package-commands.mjs";
|
||||
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
|
||||
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,28 +30,33 @@ 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;
|
||||
try {
|
||||
await withVerifierCleanup(async () => {
|
||||
const publishedRoot = await installCanaryPackage(
|
||||
`rea-agents@${version}`,
|
||||
canaryRoot,
|
||||
serverEnvironment,
|
||||
commands,
|
||||
);
|
||||
surface = await verifyInstalledCanaryPackage(
|
||||
publishedRoot,
|
||||
version,
|
||||
canaryRoot,
|
||||
serverEnvironment,
|
||||
{ environment: serverEnvironment, commands },
|
||||
);
|
||||
update = await verifyPublishedUpdate({
|
||||
version,
|
||||
canaryRoot,
|
||||
updaterRoot: publishedRoot,
|
||||
});
|
||||
} finally {
|
||||
await rm(canaryRoot, { recursive: true, force: true });
|
||||
}
|
||||
}, [() => commands.close()]);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), package: "rea-agents", version, ...surface, update })}\n`,
|
||||
);
|
||||
@@ -94,7 +99,7 @@ async function verifyPublishedUpdate({
|
||||
environment,
|
||||
);
|
||||
|
||||
await execFileAsync(
|
||||
await commands.exec(
|
||||
"npm",
|
||||
[
|
||||
"install",
|
||||
@@ -104,13 +109,20 @@ async function verifyPublishedUpdate({
|
||||
prefix,
|
||||
`rea-agents@${previousVersion}`,
|
||||
],
|
||||
{ cwd: canaryRoot, env: environment, maxBuffer: 16 * 1024 * 1024 },
|
||||
{
|
||||
cwd: canaryRoot,
|
||||
env: environment,
|
||||
timeout: 600000,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
|
||||
const npmRoot = (
|
||||
await execFileAsync("npm", ["root", "--global"], {
|
||||
await commands.exec("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
|
||||
@@ -126,9 +138,10 @@ process.stdout.write(JSON.stringify(result));
|
||||
if (result.status === "failed") process.exitCode = 1;
|
||||
`,
|
||||
);
|
||||
const { stdout } = await execFileAsync(process.execPath, [harness], {
|
||||
const { stdout } = await commands.exec(process.execPath, [harness], {
|
||||
cwd: canaryRoot,
|
||||
env: environment,
|
||||
timeout: 600000,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
});
|
||||
const result = JSON.parse(stdout);
|
||||
@@ -152,9 +165,11 @@ if (result.status === "failed") process.exitCode = 1;
|
||||
);
|
||||
|
||||
const help = (
|
||||
await execFileAsync(join(prefix, "bin", "rea-agents"), ["--help"], {
|
||||
await commands.exec(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))
|
||||
@@ -171,10 +186,10 @@ if (result.status === "failed") process.exitCode = 1;
|
||||
}
|
||||
|
||||
async function findPreviousPublishedVersion(targetVersion, cwd, environment) {
|
||||
const { stdout } = await execFileAsync(
|
||||
const { stdout } = await commands.exec(
|
||||
"npm",
|
||||
["view", "rea-agents", "versions", "--json"],
|
||||
{ cwd, env: environment, maxBuffer: 16 * 1024 * 1024 },
|
||||
{ cwd, env: environment, timeout: 30000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const versions = JSON.parse(stdout);
|
||||
const previous = versions
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
#!/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";
|
||||
@@ -46,7 +50,7 @@ transport.stderr?.on("data", (bytes) => {
|
||||
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
||||
const measurements = [];
|
||||
const contract = toolContract("inspect_web_network_capture");
|
||||
try {
|
||||
const report = await withVerifierCleanup(async () => {
|
||||
try {
|
||||
await client.connect(transport);
|
||||
} catch (cause) {
|
||||
@@ -192,7 +196,7 @@ try {
|
||||
});
|
||||
}
|
||||
await client.ping();
|
||||
const report = {
|
||||
return {
|
||||
status: "passed",
|
||||
profile: "aioquic-1.5.0-json-0.3",
|
||||
measurements,
|
||||
@@ -202,18 +206,23 @@ try {
|
||||
"No PCAP or keylog is read by this verifier; repeated STREAM coverage is not proof of retransmission or network loss.",
|
||||
],
|
||||
};
|
||||
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();
|
||||
}
|
||||
}, [
|
||||
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,
|
||||
}),
|
||||
);
|
||||
@@ -310,7 +310,6 @@ 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" });
|
||||
@@ -401,11 +400,13 @@ await withVerifierCleanup(async () => {
|
||||
stderr: join(lab, `${mode}.stderr`),
|
||||
},
|
||||
);
|
||||
const initialized = response.stdout
|
||||
const events = response.stdout
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => JSON.parse(line))
|
||||
.find((v) => v.type === "system" && v.subtype === "init");
|
||||
.map((line) => JSON.parse(line));
|
||||
const initialized = events.find(
|
||||
(v) => v.type === "system" && v.subtype === "init",
|
||||
);
|
||||
assert(initialized);
|
||||
const observed = initialized.tools
|
||||
.filter((n) => n.startsWith("mcp__rea__"))
|
||||
@@ -417,7 +418,15 @@ await withVerifierCleanup(async () => {
|
||||
"Qwen must discover the complete REA catalog",
|
||||
);
|
||||
assert.equal(fixture.failure, undefined);
|
||||
assert(finalSeen);
|
||||
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;
|
||||
if (mode === "call") {
|
||||
assert(evidenceSeen);
|
||||
assert(selectedSkillLoaded);
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#!/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 { execFile } from "node:child_process";
|
||||
import { createPackageVerifierCommands } from "./lib/verify-package-commands.mjs";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import {
|
||||
access,
|
||||
@@ -14,17 +18,16 @@ 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 execFileAsync(
|
||||
const { stdout } = await commands.exec(
|
||||
process.execPath,
|
||||
[cli, ...arguments_, "--format", "json"],
|
||||
{
|
||||
encoding: "utf8",
|
||||
timeout: 150_000,
|
||||
maxBuffer: 4 * 1024 * 1024,
|
||||
env: process.env,
|
||||
},
|
||||
@@ -56,8 +59,14 @@ 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");
|
||||
try {
|
||||
await withVerifierCleanup(async () => {
|
||||
const discovery = await runCli("list-cutter-sessions");
|
||||
assert.equal(
|
||||
discovery.discovery_status,
|
||||
@@ -111,7 +120,15 @@ try {
|
||||
});
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
try {
|
||||
const mcpOwner = {
|
||||
close: () =>
|
||||
closeVerifierResources([
|
||||
() => mcpClient.close(),
|
||||
() => mcpServer.close(),
|
||||
]),
|
||||
};
|
||||
commands.adopt(mcpOwner, "Cutter verifier MCP server");
|
||||
await withVerifierCleanup(async () => {
|
||||
await mcpServer.connect(serverTransport);
|
||||
await mcpClient.connect(clientTransport);
|
||||
const mcpDiscovery = await mcpClient.callTool({
|
||||
@@ -138,10 +155,7 @@ try {
|
||||
inspection.normalized_result,
|
||||
"Cutter CLI and MCP returned different analysis results.",
|
||||
);
|
||||
} finally {
|
||||
await mcpClient.close();
|
||||
await mcpServer.close();
|
||||
}
|
||||
}, [() => mcpOwner.close()]);
|
||||
|
||||
const annotation = `rea-smoke-${randomUUID()}`;
|
||||
await runCli(
|
||||
@@ -212,6 +226,4 @@ try {
|
||||
mode: 0o600,
|
||||
});
|
||||
console.log(JSON.stringify(report));
|
||||
} finally {
|
||||
await rm(projectRoot, { recursive: true, force: true });
|
||||
}
|
||||
}, [() => commands.close()]);
|
||||
+371
-336
@@ -1,3 +1,7 @@
|
||||
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";
|
||||
@@ -67,307 +71,116 @@ let stderr = "";
|
||||
transport.stderr?.on("data", (chunk) => {
|
||||
stderr = (stderr + chunk.toString()).slice(-65536);
|
||||
});
|
||||
let connected = false;
|
||||
let verifierRun;
|
||||
let report;
|
||||
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,
|
||||
),
|
||||
await withVerifierCleanup(async () => {
|
||||
try {
|
||||
await exec(
|
||||
ca65,
|
||||
[join(root, "tests/conformance/ghidra/6502.s"), "-o", object],
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
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",
|
||||
lowPath,
|
||||
path,
|
||||
object,
|
||||
],
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
const linked = await readFile(lowPath);
|
||||
const expectedCode = Buffer.from(
|
||||
"a907200000c90ad003a92a60a9006018690360",
|
||||
"hex",
|
||||
const bytes = await readFile(path);
|
||||
// Independently known opcodes and link addresses from the source fixture.
|
||||
assert.equal(
|
||||
bytes.toString("hex"),
|
||||
"a907201080c90ad003a92a60a9006018690360",
|
||||
);
|
||||
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,
|
||||
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
await client.connect(transport);
|
||||
connected = true;
|
||||
const opened = await call("open_binary", {
|
||||
path,
|
||||
format: "raw-binary",
|
||||
architecture: "6502",
|
||||
entry_point: lowEntry,
|
||||
entry_point: "0x008001",
|
||||
provider_id: "ghidra",
|
||||
});
|
||||
assert.equal(lowOpened.entry_point, lowEntry);
|
||||
const lowImage = evidence(
|
||||
await call("inspect_native_load_image"),
|
||||
lowSha256,
|
||||
);
|
||||
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(
|
||||
lowImage.normalized_result.status,
|
||||
image.normalized_result.status,
|
||||
"verified",
|
||||
JSON.stringify(lowImage.normalized_result),
|
||||
JSON.stringify(image.normalized_result),
|
||||
);
|
||||
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(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(fileMappings.length, 1);
|
||||
assert.equal(
|
||||
fileMappings[0].address_space,
|
||||
lowImage.normalized_result.observations.default_address_space,
|
||||
const instruction = evidence(
|
||||
await call("inspect_native_instruction", { address: "0x8003" }),
|
||||
sha256,
|
||||
);
|
||||
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.equal(instruction.normalized_result.mnemonic.toUpperCase(), "JSR");
|
||||
assert.ok(
|
||||
lowCall.normalized_result.references.some(
|
||||
(reference) =>
|
||||
reference.call &&
|
||||
reference.target_address === `0x${(entry + 15).toString(16)}`,
|
||||
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");
|
||||
const lowCli = await exec(
|
||||
await client.ping();
|
||||
const { stdout } = 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,
|
||||
});
|
||||
}
|
||||
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",
|
||||
@@ -376,88 +189,310 @@ try {
|
||||
"0x8001",
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--snapshot",
|
||||
snapshotPath,
|
||||
"--json",
|
||||
];
|
||||
const saved = await exec(process.execPath, cliArgs(path), {
|
||||
env,
|
||||
timeout: 240000,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
});
|
||||
assert.equal(
|
||||
evidence(JSON.parse(saved.stdout), sha256).operation,
|
||||
"binary_overview",
|
||||
],
|
||||
{ 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",
|
||||
lowPath,
|
||||
object,
|
||||
],
|
||||
{ timeout: 10000 },
|
||||
);
|
||||
const savedSnapshot = JSON.parse(await readFile(snapshotPath, "utf8"));
|
||||
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,
|
||||
format: "raw-binary",
|
||||
architecture: "6502",
|
||||
entry_point: lowEntry,
|
||||
provider_id: "ghidra",
|
||||
});
|
||||
assert.equal(lowOpened.entry_point, lowEntry);
|
||||
const lowImage = evidence(
|
||||
await call("inspect_native_load_image"),
|
||||
lowSha256,
|
||||
);
|
||||
assert.equal(
|
||||
lowImage.normalized_result.status,
|
||||
"verified",
|
||||
JSON.stringify(lowImage.normalized_result),
|
||||
);
|
||||
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(
|
||||
savedSnapshot.workflow_entries.some(
|
||||
({ operation }) => operation === "binary_overview",
|
||||
lowCall.normalized_result.references.some(
|
||||
(reference) =>
|
||||
reference.call &&
|
||||
reference.target_address === `0x${(entry + 15).toString(16)}`,
|
||||
),
|
||||
);
|
||||
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), []);
|
||||
// Replaying a matching snapshot must still reach native path admission.
|
||||
await assert.rejects(
|
||||
exec(process.execPath, cliArgs(linkedPath), {
|
||||
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: "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), {
|
||||
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);
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}, [
|
||||
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);
|
||||
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",
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#!/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";
|
||||
@@ -61,6 +63,7 @@ for (const name of [
|
||||
"REA_GHIDRA_DISABLED_ANALYZERS",
|
||||
])
|
||||
delete env[name];
|
||||
const compilerCommands = await createVerifierCommands();
|
||||
const report = {
|
||||
verifier: "ghidra-owned-checkpoints",
|
||||
phases: [],
|
||||
@@ -119,6 +122,7 @@ 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",
|
||||
@@ -130,23 +134,15 @@ async function connect(environment = {}) {
|
||||
stderr: "pipe",
|
||||
});
|
||||
transport.stderr?.on("data", () => {});
|
||||
try {
|
||||
await client.connect(transport);
|
||||
} catch (cause) {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
throw cause;
|
||||
}
|
||||
compilerCommands.adopt(owner, "checkpoint MCP connection");
|
||||
await owner.open(client, transport);
|
||||
const raw = (name, args = {}) =>
|
||||
client.callTool({ name, arguments: args }, { timeout: 240000 });
|
||||
return {
|
||||
raw,
|
||||
call: async (name, args = {}) =>
|
||||
requireMcpOperationResult(await raw(name, args), name),
|
||||
close: async () => {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
},
|
||||
close: () => owner.close(),
|
||||
};
|
||||
}
|
||||
let manifestPath;
|
||||
@@ -160,15 +156,11 @@ __attribute__((noinline)) int rea_checkpoint_leaf(CheckpointRecord *record) { re
|
||||
int main(void) { CheckpointRecord record = { 3, 11 }; return rea_checkpoint_leaf(&record); }
|
||||
`,
|
||||
);
|
||||
await exec("cc", [
|
||||
"-O0",
|
||||
"-g",
|
||||
"-gdwarf-4",
|
||||
"-fno-inline",
|
||||
sourcePath,
|
||||
"-o",
|
||||
target,
|
||||
]);
|
||||
await compilerCommands.exec(
|
||||
"cc",
|
||||
["-O0", "-g", "-gdwarf-4", "-fno-inline", sourcePath, "-o", target],
|
||||
{ timeout: 120_000, maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const targetDigest = await sha(target);
|
||||
report.target = {
|
||||
sha256: targetDigest,
|
||||
@@ -466,9 +458,10 @@ 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) {
|
||||
|
||||
+392
-363
@@ -1,4 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
import {
|
||||
closeVerifierResources,
|
||||
withVerifierCleanup,
|
||||
} from "./lib/verifier-cleanup.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
requireMcpLifecycleResult,
|
||||
@@ -7,7 +11,14 @@ import {
|
||||
} from "./lib/mcp-verifier-results.mjs";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import {
|
||||
mkdtemp,
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { isAbsolute, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -88,386 +99,404 @@ 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);
|
||||
});
|
||||
try {
|
||||
await client.connect(transport);
|
||||
await withVerifierCleanup(async () => {
|
||||
try {
|
||||
await client.connect(transport);
|
||||
connected = true;
|
||||
|
||||
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 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 response = await client.callTool(
|
||||
{ name: "inspect_address_context", arguments: input },
|
||||
{ timeout: 240000, toolDefinition: contextDefinition },
|
||||
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 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", {
|
||||
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,
|
||||
})
|
||||
).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(
|
||||
(
|
||||
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",
|
||||
})
|
||||
).procedure.name,
|
||||
changes.name,
|
||||
);
|
||||
const rejected = await client.callTool({
|
||||
name: "annotate_native_function",
|
||||
arguments: {
|
||||
}),
|
||||
);
|
||||
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: 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", {
|
||||
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,
|
||||
})
|
||||
).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 },
|
||||
).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,
|
||||
);
|
||||
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.equal(
|
||||
await call("address_name", { address: fixture.entry }),
|
||||
changes.name,
|
||||
);
|
||||
await client.close();
|
||||
await transport.close();
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
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 });
|
||||
},
|
||||
]);
|
||||
assert.ok(report);
|
||||
const verifierRun = await completeVerifierRun(run);
|
||||
verifierRun = await completeVerifierRun(run);
|
||||
assert.equal(verifierRun.process_lineage.status, "verified");
|
||||
assert.deepEqual(verifierRun.process_lineage.descendants, []);
|
||||
process.stdout.write(
|
||||
|
||||
@@ -115,9 +115,12 @@ 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 }, undefined, {
|
||||
timeout: 180000,
|
||||
});
|
||||
const reply = await client.callTool(
|
||||
{ name, arguments: args },
|
||||
{
|
||||
timeout: 180000,
|
||||
},
|
||||
);
|
||||
return requireMcpOperationResult(reply, name);
|
||||
};
|
||||
const report = { label, language_override: languageOverride ?? null };
|
||||
|
||||
+424
-407
@@ -1,4 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
import {
|
||||
closeVerifierResources,
|
||||
withVerifierCleanup,
|
||||
} from "./lib/verifier-cleanup.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
requireMcpOperationResult,
|
||||
@@ -99,427 +103,440 @@ 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;
|
||||
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,
|
||||
),
|
||||
},
|
||||
});
|
||||
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,
|
||||
);
|
||||
assertBody(info.body, procedure);
|
||||
assertBody(dossier.procedure.body, procedure);
|
||||
assert.equal(info.entrypoint, procedure);
|
||||
assert.equal(info.length, info.body.total_bytes);
|
||||
assert.deepEqual(dossier.procedure.body, info.body);
|
||||
const containingQueries = [procedure, info.body.ranges.at(-1).end];
|
||||
for (const query of containingQueries) {
|
||||
const containing = await call("resolve_containing_procedure", {
|
||||
address: query,
|
||||
});
|
||||
assert.equal(containing.found, true);
|
||||
assert.equal(containing.query_address, query);
|
||||
assert.equal(containing.procedure.address, procedure);
|
||||
assert.deepEqual(containing.procedure.body, info.body);
|
||||
}
|
||||
if (key === "entry") {
|
||||
const gap = await call("resolve_containing_procedure", {
|
||||
address: address(0x10),
|
||||
});
|
||||
assert.equal(
|
||||
gap.found,
|
||||
false,
|
||||
"Gap inside enclosing span was treated as function body",
|
||||
);
|
||||
assert.equal(gap.reason, "not_in_procedure");
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
dossier.pseudocode.trim().length > 0,
|
||||
"Real DOS decompilation returned empty pseudocode",
|
||||
);
|
||||
assert.equal(dossier.procedure.address, procedure);
|
||||
const window = await call("read_function_instructions", { procedure });
|
||||
assert.deepEqual(window.procedure.body, info.body);
|
||||
assert.ok(window.instructions.length > 0);
|
||||
if (key === "entry") {
|
||||
assert.deepEqual(info.body.ranges, [
|
||||
{ start: procedure, end: address(fixture.entry_first_bytes - 1) },
|
||||
{
|
||||
start: address(fixture.entry_tail_offset),
|
||||
end: address(
|
||||
fixture.entry_tail_offset + fixture.entry_tail_bytes - 1,
|
||||
),
|
||||
},
|
||||
]);
|
||||
assert.equal(
|
||||
info.body.total_bytes,
|
||||
fixture.entry_first_bytes + fixture.entry_tail_bytes,
|
||||
);
|
||||
assert.equal(
|
||||
info.body.span_bytes,
|
||||
fixture.entry_tail_offset + fixture.entry_tail_bytes,
|
||||
);
|
||||
assert.equal(info.body.non_contiguous, true);
|
||||
} else {
|
||||
const size =
|
||||
key === "near" ? fixture.near_body_bytes : fixture.far_body_bytes;
|
||||
assert.deepEqual(info.body.ranges, [
|
||||
{
|
||||
start: procedure,
|
||||
end: address(
|
||||
(key === "near" ? fixture.near_offset : fixture.far_offset) +
|
||||
size -
|
||||
1,
|
||||
),
|
||||
},
|
||||
]);
|
||||
assert.equal(info.body.total_bytes, size);
|
||||
assert.equal(info.body.span_bytes, size);
|
||||
}
|
||||
const cli = await cliEvidence("function", procedure);
|
||||
if (captureDirectory !== undefined) {
|
||||
await writeFile(
|
||||
join(captureDirectory, `public-fixture-${key}-cli.json`),
|
||||
`${JSON.stringify(cli, null, 2)}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
await writeFile(
|
||||
join(captureDirectory, `public-fixture-${key}-mcp.json`),
|
||||
`${JSON.stringify(dossierResult, null, 2)}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
}
|
||||
const cliDossier = functionDossierSchema.parse(cli.normalized_result);
|
||||
assert.deepEqual(
|
||||
stableDossierObservations(cliDossier),
|
||||
stableDossierObservations(dossier),
|
||||
"CLI/MCP stable DOS dossier observations differ",
|
||||
);
|
||||
facts[key] = {
|
||||
address: procedure,
|
||||
body: info.body,
|
||||
instruction_count: window.instructions.length,
|
||||
pseudocode_nonempty: true,
|
||||
pseudocode: dossier.pseudocode,
|
||||
cli_mcp_observations_equal: true,
|
||||
containing_procedure_body_equal: true,
|
||||
limitations: dossier.limitations,
|
||||
};
|
||||
}
|
||||
const move = await call("inspect_native_instruction", {
|
||||
address: identities.entry.address,
|
||||
});
|
||||
assert.equal(move.status, "decoded");
|
||||
assert.equal(move.length, 3);
|
||||
assert.equal(hexBytes(move.bytes), "b83412");
|
||||
assert.match(move.mnemonic, /^MOV$/iu);
|
||||
assert.ok(
|
||||
move.operands
|
||||
.flatMap((operand) => operand.components)
|
||||
.some(
|
||||
(part) =>
|
||||
part.kind === "register" &&
|
||||
/^AX$/iu.test(part.text) &&
|
||||
part.bit_width === 16,
|
||||
),
|
||||
"DOS entry was not decoded as 16-bit AX",
|
||||
);
|
||||
const nearMemory = await call("inspect_native_instruction", {
|
||||
address: identities.near.address,
|
||||
});
|
||||
assert.equal(nearMemory.status, "decoded");
|
||||
assert.equal(nearMemory.length, 2);
|
||||
assert.equal(hexBytes(nearMemory.bytes), "fe00");
|
||||
assert.match(nearMemory.mnemonic, /^INC$/iu);
|
||||
const nearCall = await call("inspect_native_instruction", {
|
||||
address: address(fixture.near_call_offset),
|
||||
});
|
||||
const farCall = await call("inspect_native_instruction", {
|
||||
address: address(fixture.far_call_offset),
|
||||
});
|
||||
assert.equal(hexBytes(nearCall.bytes), "e81a00");
|
||||
// Source far segment 0004 is relocated by the independently fixed 1000 load segment.
|
||||
assert.equal(hexBytes(farCall.bytes), "9a00000410");
|
||||
for (const [key, instruction] of [
|
||||
["near", nearCall],
|
||||
["far", farCall],
|
||||
]) {
|
||||
assert.equal(instruction.flow.kind, "call");
|
||||
assert.ok(
|
||||
instruction.flow.direct_destinations.includes(identities[key].address),
|
||||
);
|
||||
const targets = await call("resolve_native_call_targets", {
|
||||
address: instruction.address,
|
||||
});
|
||||
assert.equal(targets.status, "direct");
|
||||
assert.ok(
|
||||
targets.targets.some(
|
||||
(target) => target.address === identities[key].address,
|
||||
),
|
||||
);
|
||||
}
|
||||
const invalid = await client.callTool({
|
||||
name: "procedure_info",
|
||||
arguments: { procedure: "0xffffffffffffffff" },
|
||||
});
|
||||
assert.equal(invalid.isError, true, "Unmapped address did not fail");
|
||||
// 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 {
|
||||
await withVerifierCleanup(async () => {
|
||||
try {
|
||||
if (opened) {
|
||||
try {
|
||||
await call("close_binary");
|
||||
} catch (error) {
|
||||
process.stderr.write(`DOS cleanup close failed: ${String(error)}\n`);
|
||||
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",
|
||||
),
|
||||
"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,
|
||||
);
|
||||
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,
|
||||
};
|
||||
}
|
||||
try {
|
||||
await client.close();
|
||||
} finally {
|
||||
await 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");
|
||||
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;
|
||||
}
|
||||
}, [
|
||||
async () => {
|
||||
await closeVerifierResources([
|
||||
async () => {
|
||||
if (connected) await call("close_binary", {});
|
||||
},
|
||||
() => client.close(),
|
||||
() => transport.close(),
|
||||
]);
|
||||
assert.equal(
|
||||
digest(await readFile(targetPath)),
|
||||
sha256,
|
||||
"Original changed during cleanup",
|
||||
);
|
||||
const leftovers = (await readdir(runtime)).filter((name) =>
|
||||
name.startsWith("rea-ghidra-"),
|
||||
assert.deepEqual(
|
||||
(await readdir(runtime)).filter((name) => name.startsWith("rea-ghidra-")),
|
||||
[],
|
||||
"Owned Ghidra runtime survived close",
|
||||
);
|
||||
assert.deepEqual(leftovers, [], "Owned Ghidra runtime survived close");
|
||||
} finally {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
},
|
||||
]);
|
||||
assert.ok(report, "No DOS proof report was produced");
|
||||
const verifierRun = await completeVerifierRun(run);
|
||||
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(
|
||||
|
||||
@@ -284,7 +284,6 @@ try {
|
||||
const call = async (name, args = {}) => {
|
||||
const reply = await client.callTool(
|
||||
{ name, arguments: args },
|
||||
undefined,
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
return requireMcpOperationResult(reply, name);
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
import {
|
||||
closeVerifierResources,
|
||||
withVerifierCleanup,
|
||||
} from "./lib/verifier-cleanup.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
requireMcpLifecycleResult,
|
||||
@@ -24,6 +28,8 @@ 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 (
|
||||
![
|
||||
@@ -152,248 +158,268 @@ transport.stderr?.on("data", (chunk) => {
|
||||
stderr = (stderr + chunk.toString()).slice(-65536);
|
||||
});
|
||||
let opened = false;
|
||||
let connected = false;
|
||||
let verifierRun;
|
||||
let report;
|
||||
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) {
|
||||
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));
|
||||
opened = true;
|
||||
failure = await client.callTool(
|
||||
{ name: "inspect_native_load_image", arguments: {} },
|
||||
{ timeout: 360000 },
|
||||
const image = nativeLoadImageSchema.parse(
|
||||
await call("inspect_native_load_image"),
|
||||
);
|
||||
}
|
||||
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));
|
||||
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 };
|
||||
assert.equal(image.observations.metadata_recovery, undefined);
|
||||
report = { mode, target, sha256, image };
|
||||
} 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.notEqual(response.isError, true, JSON.stringify(response));
|
||||
opened = true;
|
||||
const image = nativeLoadImageSchema.parse(
|
||||
await call("inspect_native_load_image"),
|
||||
);
|
||||
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 =
|
||||
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"
|
||||
? 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)
|
||||
? 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(
|
||||
(await call("search_strings", { pattern: literal })).length > 0,
|
||||
`Frozen string missing: ${literal}`,
|
||||
metadata.interfaces.some((type) => type.address === interfaceAddress),
|
||||
);
|
||||
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 },
|
||||
assert.ok(
|
||||
metadata.virtual_slots.some(
|
||||
(slot) => slot.target_address === addressOf(fixtureSymbols.compute),
|
||||
),
|
||||
);
|
||||
const evidence = parseEvidence(JSON.parse(stdout));
|
||||
assert.equal(evidence.subject.digest.sha256, sha256);
|
||||
assert.deepEqual(
|
||||
evidence.normalized_result.metadata_recovery,
|
||||
metadata,
|
||||
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",
|
||||
);
|
||||
report.cli_metadata = evidence.normalized_result.metadata_recovery;
|
||||
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",
|
||||
);
|
||||
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",
|
||||
assert.equal(
|
||||
hash(await readFile(target)),
|
||||
sha256,
|
||||
"Input executable changed",
|
||||
);
|
||||
if (opened) {
|
||||
await call("close_binary");
|
||||
opened = false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(stderr);
|
||||
throw error;
|
||||
}
|
||||
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" : ""}`,
|
||||
}, [
|
||||
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",
|
||||
);
|
||||
} 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");
|
||||
}
|
||||
|
||||
@@ -92,7 +92,6 @@ try {
|
||||
const call = async (name, args = {}) => {
|
||||
const reply = await client.callTool(
|
||||
{ name, arguments: args },
|
||||
undefined,
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
return requireMcpOperationResult(reply, name);
|
||||
|
||||
+487
-453
@@ -29,7 +29,10 @@ import {
|
||||
requireMcpOutput,
|
||||
} from "./lib/mcp-verifier-results.mjs";
|
||||
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
|
||||
import {
|
||||
withVerifierCleanup,
|
||||
closeVerifierResources,
|
||||
} from "./lib/verifier-cleanup.mjs";
|
||||
|
||||
if (process.argv.length !== 2)
|
||||
throw new Error("Usage: node scripts/verify-real-ghidra-raw.mjs");
|
||||
@@ -102,490 +105,521 @@ let stderr = "";
|
||||
transport.stderr?.on("data", (chunk) => {
|
||||
stderr = (stderr + chunk.toString()).slice(-65536);
|
||||
});
|
||||
let connected = false;
|
||||
let verifierRun;
|
||||
let report;
|
||||
let searchDefinition;
|
||||
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 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),
|
||||
await withVerifierCleanup(async () => {
|
||||
try {
|
||||
await client.connect(transport);
|
||||
connected = true;
|
||||
searchDefinition = (await client.listTools()).tools.find(
|
||||
(tool) => tool.name === "search_bytes",
|
||||
);
|
||||
assert.equal(image.normalized_result.base_address, entry);
|
||||
assert.equal(image.normalized_result.entry.address, entry);
|
||||
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",
|
||||
});
|
||||
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;
|
||||
assert.equal(
|
||||
image.normalized_result.observations.language_id,
|
||||
"ARM:LE:32:v7",
|
||||
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(
|
||||
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,
|
||||
retained.normalized_result.comments.some(
|
||||
(comment) => comment.text === "Owned session annotation",
|
||||
),
|
||||
);
|
||||
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",
|
||||
});
|
||||
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;
|
||||
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");
|
||||
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);
|
||||
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",
|
||||
});
|
||||
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: x86Path,
|
||||
path: repeatedPath,
|
||||
format: "raw-binary",
|
||||
architecture,
|
||||
architecture: "x86_64",
|
||||
entry_point: "0x1000",
|
||||
});
|
||||
const image = await call("inspect_native_load_image", {}, x86Sha256);
|
||||
// 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(
|
||||
image.normalized_result.status,
|
||||
"verified",
|
||||
JSON.stringify(image),
|
||||
rest.normalized_result.matches[0].address,
|
||||
`0x${(0x1000 + 6000).toString(16)}`,
|
||||
);
|
||||
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(rest.normalized_result.complete, true);
|
||||
const masked = await call(
|
||||
"search_bytes",
|
||||
{ pattern: "bf", mask: "f0", start: "0x1000", end: "0x1005" },
|
||||
repeatedSha256,
|
||||
);
|
||||
assert.equal(memory.normalized_result.bytes_hex, x86Bytes.toString("hex"));
|
||||
const pseudo = await call(
|
||||
"procedure_pseudo_code",
|
||||
{ procedure: "0x1000" },
|
||||
x86Sha256,
|
||||
assert.deepEqual(
|
||||
masked.normalized_result.matches.map((match) => match.address),
|
||||
["0x1000"],
|
||||
);
|
||||
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 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 },
|
||||
});
|
||||
}
|
||||
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);
|
||||
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(
|
||||
// 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(
|
||||
"search_bytes",
|
||||
{
|
||||
pattern,
|
||||
start: "0x401000",
|
||||
end: "0x401007",
|
||||
limit: 2,
|
||||
...(cursor === undefined ? {} : { cursor }),
|
||||
end: "0x401005",
|
||||
limit: 20,
|
||||
},
|
||||
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(
|
||||
"search_bytes",
|
||||
{
|
||||
pattern,
|
||||
start: "0x401000",
|
||||
end: "0x401005",
|
||||
limit: 20,
|
||||
},
|
||||
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",
|
||||
);
|
||||
assert.deepEqual(
|
||||
bounded.normalized_result.matches.map((match) => match.address),
|
||||
addresses.slice(0, 4),
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}, [
|
||||
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",
|
||||
);
|
||||
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(verifierRun.process_lineage.status, "verified");
|
||||
assert.deepEqual(verifierRun.process_lineage.descendants, []);
|
||||
process.stdout.write(
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
#!/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 {
|
||||
@@ -71,9 +75,11 @@ const literalObjects = ["decoder-safeguards.S", "unresolved-string-jump.s"].map(
|
||||
}),
|
||||
);
|
||||
const reports = [];
|
||||
let connected = false;
|
||||
let verifierRun;
|
||||
let literalReport;
|
||||
let decoderCleaned = true;
|
||||
try {
|
||||
await withVerifierCleanup(async () => {
|
||||
await execute("swiftc", ["-O", "-g", source, "-o", binary], {
|
||||
timeout: 60_000,
|
||||
});
|
||||
@@ -110,12 +116,13 @@ try {
|
||||
);
|
||||
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" });
|
||||
try {
|
||||
await withVerifierCleanup(async () => {
|
||||
const procedures = await call("list_procedures", {});
|
||||
const inventory = await call("analyze_swift_types", {});
|
||||
assert(
|
||||
@@ -183,16 +190,25 @@ try {
|
||||
metadata_accessors: metadata.total,
|
||||
unclassified: inventory.unclassified.length,
|
||||
});
|
||||
} finally {
|
||||
await call("close_binary", {});
|
||||
}
|
||||
}, [() => call("close_binary", {})]);
|
||||
}
|
||||
literalReport = await verifyStringLiterals();
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
if (decoderCleaned) await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}, [
|
||||
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 });
|
||||
},
|
||||
]);
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
@@ -200,7 +216,7 @@ console.log(
|
||||
provider: "ghidra",
|
||||
fixtures: reports,
|
||||
swift_string_literals: literalReport,
|
||||
verifier_run: await completeVerifierRun(run),
|
||||
verifier_run: verifierRun,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
@@ -220,7 +236,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" });
|
||||
try {
|
||||
return withVerifierCleanup(async () => {
|
||||
const procedures = await call("list_procedures", {});
|
||||
const expected = {
|
||||
smallLiteral: [["SleepDisabled", "small"]],
|
||||
@@ -281,9 +297,7 @@ async function verifyStringLiterals() {
|
||||
decoded.unresolvedStringJump =
|
||||
unresolvedDossier.swift_string_literal_omissions.length;
|
||||
return decoded;
|
||||
} finally {
|
||||
await call("close_binary", {});
|
||||
}
|
||||
}, [() => call("close_binary", {})]);
|
||||
}
|
||||
|
||||
// Exercise the Java producer inside an ephemeral Ghidra database. The explicit
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
requireMcpOperationResult,
|
||||
@@ -95,7 +96,7 @@ await writeFile(source, buildSwitchSource());
|
||||
const flags = ["-O2", "-g", "-fno-pie", "-no-pie"];
|
||||
const results = [];
|
||||
let productionControls = null;
|
||||
try {
|
||||
await withVerifierCleanup(async () => {
|
||||
for (const compiler of ["gcc", "clang"]) {
|
||||
const original = join(workspace, `${compiler}-optimized`);
|
||||
await exec(commands[compiler], [...flags, source, "-o", original]);
|
||||
@@ -169,17 +170,19 @@ try {
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
}, [
|
||||
async () => {
|
||||
assert.deepEqual(
|
||||
(await readdir(runtime)).filter((name) => name.startsWith("rea-ghidra-")),
|
||||
[],
|
||||
"Owned Ghidra runtime survived close",
|
||||
);
|
||||
} finally {
|
||||
const cleanup = await completeVerifierRun(run);
|
||||
assert.equal(cleanup.process_lineage.status, "verified");
|
||||
assert.deepEqual(cleanup.process_lineage.descendants, []);
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
},
|
||||
]);
|
||||
const lifecycle = await completeVerifierRun(run);
|
||||
if (lifecycle.process_lineage.status === "verified")
|
||||
assert.deepEqual(lifecycle.process_lineage.descendants, []);
|
||||
@@ -212,7 +215,7 @@ async function verifyTarget(path, sha256, fixtures, variant) {
|
||||
let opened = false;
|
||||
let stderr = "";
|
||||
const observations = [];
|
||||
try {
|
||||
return withVerifierCleanup(async () => {
|
||||
await client.connect(transport);
|
||||
transport.stderr?.on("data", (chunk) => {
|
||||
stderr = (stderr + chunk.toString()).slice(-65536);
|
||||
@@ -287,17 +290,13 @@ async function verifyTarget(path, sha256, fixtures, variant) {
|
||||
});
|
||||
}
|
||||
return { observations, stderr };
|
||||
} finally {
|
||||
try {
|
||||
}, [
|
||||
async () => {
|
||||
if (opened) await call(client, "close_binary");
|
||||
} finally {
|
||||
try {
|
||||
await client.close();
|
||||
} finally {
|
||||
await transport.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
() => client.close(),
|
||||
() => transport.close(),
|
||||
]);
|
||||
}
|
||||
function assertWorkflowIdentity(evidence, sha256) {
|
||||
assert.equal(evidence.subject.digest.sha256, sha256);
|
||||
|
||||
@@ -196,17 +196,6 @@ 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);
|
||||
@@ -216,9 +205,19 @@ try {
|
||||
assert.equal(noNotes.threads.length, 0);
|
||||
assert.equal(noNotes.note_interpretation_completeness, "unknown");
|
||||
cases++;
|
||||
for (const [name, architecture, type] of [
|
||||
["aarch64-archext", "aarch64-little-endian", "PT_AARCH64_ARCHEXT"],
|
||||
["aarch64-unwind", "aarch64-little-endian", "PT_AARCH64_UNWIND"],
|
||||
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,
|
||||
],
|
||||
["aarch64-mte-opaque", "aarch64-little-endian", 0x70000002],
|
||||
["amd64-processor-opaque", "x86_64-little-endian", 0x70000001],
|
||||
]) {
|
||||
@@ -226,6 +225,8 @@ 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");
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
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";
|
||||
@@ -26,22 +28,25 @@ 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;
|
||||
try {
|
||||
await withVerifierCleanup(async () => {
|
||||
const packageRoot = await installCanaryPackage(
|
||||
source,
|
||||
workspace,
|
||||
environment,
|
||||
commands,
|
||||
);
|
||||
result = await verifyInstalledCanaryPackage(
|
||||
packageRoot,
|
||||
version,
|
||||
workspace,
|
||||
environment,
|
||||
);
|
||||
} finally {
|
||||
await rm(workspace, { recursive: true, force: true });
|
||||
}
|
||||
result = await verifyInstalledCanaryPackage(packageRoot, version, workspace, {
|
||||
environment: environment,
|
||||
commands,
|
||||
});
|
||||
}, [() => commands.close()]);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), package: "rea-agents", version, source, ...result })}\n`,
|
||||
);
|
||||
+709
-701
File diff suppressed because it is too large.
Load diff
@@ -66,7 +66,18 @@ try {
|
||||
} else {
|
||||
// Overview controls verify identity, ABI and snapshot section identities;
|
||||
// they deliberately do not require any printable string values.
|
||||
assert.ok(result.libapp.sections.length > 0);
|
||||
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",
|
||||
);
|
||||
}
|
||||
const tokenEstimate = calls.reduce(
|
||||
(n, call) => n + call.request_token_estimate + call.reply_token_estimate,
|
||||
|
||||
@@ -13,14 +13,28 @@ 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 sorted(options.inputs.glob('*.metrics.json')):
|
||||
for path in paths:
|
||||
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'])))),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createPackageVerifierCommands } from "../../lib/verify-package-commands.mjs";
|
||||
import { withVerifierCleanup } from "../../lib/verifier-cleanup.mjs";
|
||||
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,8 +19,6 @@ 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-"));
|
||||
|
||||
@@ -67,8 +65,19 @@ 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;
|
||||
try {
|
||||
await withVerifierCleanup(async () => {
|
||||
const { app, executable } = await buildMacosBundleFixture(root);
|
||||
const nativeProvenance = await verifyNativeProvenance(app, executable);
|
||||
const machoOccurrences = await verifyMachOOccurrences(app, executable);
|
||||
@@ -173,7 +182,5 @@ try {
|
||||
dylib_resolution: dylibs,
|
||||
dyld_environment: dyldEnvironment,
|
||||
};
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}, [() => commands.close()]);
|
||||
process.stdout.write(`${JSON.stringify(report)}\n`);
|
||||
@@ -1,10 +1,10 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createPackageVerifierCommands } from "../../lib/verify-package-commands.mjs";
|
||||
import { withVerifierCleanup } from "../../lib/verifier-cleanup.mjs";
|
||||
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,8 +14,6 @@ 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" },
|
||||
@@ -182,7 +180,18 @@ export async function verifyNativeProvenance(app, executable) {
|
||||
async function verifyInstallNames() {
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-macho-install-names-"));
|
||||
const observations = [];
|
||||
try {
|
||||
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 () => {
|
||||
const librarySource = join(root, "library.c");
|
||||
const consumerSource = join(root, "consumer.c");
|
||||
await writeFile(librarySource, "int exported_value(void) { return 42; }\n");
|
||||
@@ -279,7 +288,5 @@ async function verifyInstallNames() {
|
||||
});
|
||||
}
|
||||
return observations;
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}, [() => commands.close()]);
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
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";
|
||||
@@ -92,207 +96,221 @@ bridge.exposeInMainWorld("fixture", {
|
||||
});
|
||||
let application;
|
||||
let stderr = "";
|
||||
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_ },
|
||||
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 },
|
||||
{ timeout: 150_000 },
|
||||
);
|
||||
assert.notEqual(result.isError, true, JSON.stringify(result));
|
||||
const validate = validators.get(name);
|
||||
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(
|
||||
validate?.(result.structuredContent),
|
||||
JSON.stringify(validate?.errors),
|
||||
capture.ipc.events.some(
|
||||
({ kind, channel }) =>
|
||||
kind === "main-handler-invocation" && channel === "fixture:echo",
|
||||
),
|
||||
JSON.stringify(capture.ipc),
|
||||
);
|
||||
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 },
|
||||
{ timeout: 150_000 },
|
||||
);
|
||||
const activeError = active.isError
|
||||
? requireMcpToolError(active)
|
||||
: undefined;
|
||||
if (activeError !== undefined)
|
||||
assert.equal(
|
||||
activeError.code,
|
||||
"cleanup_incomplete",
|
||||
JSON.stringify(activeError),
|
||||
capture.coverage.status,
|
||||
"partial_attach",
|
||||
JSON.stringify(capture),
|
||||
);
|
||||
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),
|
||||
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 },
|
||||
);
|
||||
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 });
|
||||
}
|
||||
}, [
|
||||
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));
|
||||
}
|
||||
},
|
||||
]);
|
||||
await rm(root, { recursive: true, force: true });
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const pause = (milliseconds) =>
|
||||
@@ -345,7 +363,7 @@ async function verifyCancellation({ input, ready, entrypoint, root }) {
|
||||
child.once("exit", (code, signal) => resolve({ code, signal }));
|
||||
});
|
||||
let ownership;
|
||||
try {
|
||||
await withVerifierCleanup(async () => {
|
||||
const deadline = Date.now() + 30_000;
|
||||
while (Date.now() < deadline) {
|
||||
const table = (
|
||||
@@ -419,14 +437,35 @@ 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}`,
|
||||
);
|
||||
} 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));
|
||||
}
|
||||
}
|
||||
}, [
|
||||
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));
|
||||
}
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -1,17 +1,29 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { createPackageVerifierCommands } from "../../lib/verify-package-commands.mjs";
|
||||
import { withVerifierCleanup } from "../../lib/verifier-cleanup.mjs";
|
||||
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-"));
|
||||
try {
|
||||
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/);
|
||||
|
||||
const source = join(root, "board.dts"),
|
||||
blob = join(root, "board.dtb");
|
||||
writeFileSync(
|
||||
@@ -30,16 +42,15 @@ try {
|
||||
consumer { remote-endpoint = <&serial>; };
|
||||
};\n`,
|
||||
);
|
||||
execFileSync(dtc, ["-I", "dts", "-O", "dtb", "-o", blob, source]);
|
||||
const decompiled = execFileSync(dtc, ["-I", "dtb", "-O", "dts", blob], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
await exec(dtc, ["-I", "dts", "-O", "dtb", "-o", blob, source]);
|
||||
const decompiled = await exec(dtc, ["-I", "dtb", "-O", "dts", blob]);
|
||||
const evidence = JSON.parse(
|
||||
execFileSync(
|
||||
process.execPath,
|
||||
["scripts/rea.mjs", "inspect-devicetree", blob, "--json"],
|
||||
{ encoding: "utf8" },
|
||||
),
|
||||
await exec(process.execPath, [
|
||||
"scripts/rea.mjs",
|
||||
"inspect-devicetree",
|
||||
blob,
|
||||
"--json",
|
||||
]),
|
||||
);
|
||||
const report = evidence.normalized_result;
|
||||
// This controlled fixture uses one opening/closing brace per node line.
|
||||
@@ -83,6 +94,4 @@ try {
|
||||
2,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
}, [() => commands.close()]);
|
||||
@@ -1,3 +1,7 @@
|
||||
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";
|
||||
@@ -38,7 +42,7 @@ const names = [
|
||||
...(process.platform === "win32" ? [] : ['with"quote.cjs']),
|
||||
];
|
||||
|
||||
try {
|
||||
await withVerifierCleanup(async () => {
|
||||
await client.connect(transport);
|
||||
for (const name of names) {
|
||||
const targetPath =
|
||||
@@ -54,19 +58,20 @@ try {
|
||||
stdio: ["ignore", "ignore", "pipe"],
|
||||
},
|
||||
);
|
||||
try {
|
||||
await verifyTarget(child, targetPath, name);
|
||||
} finally {
|
||||
await terminate(child);
|
||||
}
|
||||
await withVerifierCleanup(
|
||||
() => verifyTarget(child, targetPath, name),
|
||||
[() => terminate(child)],
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
await client.close();
|
||||
} finally {
|
||||
}, [
|
||||
async () => {
|
||||
await closeVerifierResources([
|
||||
() => client.close(),
|
||||
() => transport.close(),
|
||||
]);
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
},
|
||||
]);
|
||||
|
||||
async function verifyTarget(child, targetPath, name) {
|
||||
const socket = new URL(await inspectorUrl(child));
|
||||
@@ -192,15 +197,37 @@ async function runMcp(name, arguments_) {
|
||||
}
|
||||
|
||||
async function inspectorUrl(process_) {
|
||||
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");
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
async function runCli(arguments_) {
|
||||
@@ -236,7 +263,14 @@ async function terminate(process_) {
|
||||
)
|
||||
return;
|
||||
process_.kill("SIGKILL");
|
||||
await exited;
|
||||
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");
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
#!/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";
|
||||
@@ -51,7 +55,7 @@ const call = async (name, args) => {
|
||||
const fingerprint = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
const counts = [];
|
||||
let finalReport;
|
||||
try {
|
||||
await withVerifierCleanup(async () => {
|
||||
const fixtures = await buildRecoveryFixtures(
|
||||
root,
|
||||
process.env.REA_JAVASCRIPT_FIXTURE_TOOLS,
|
||||
@@ -165,11 +169,15 @@ try {
|
||||
results: counts,
|
||||
verified: true,
|
||||
};
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}, [
|
||||
async () => {
|
||||
await closeVerifierResources([
|
||||
() => client.close(),
|
||||
() => transport.close(),
|
||||
]);
|
||||
await rm(root, { recursive: true, force: true });
|
||||
},
|
||||
]);
|
||||
console.log(JSON.stringify(finalReport));
|
||||
|
||||
async function assertRecoveredArtifacts(evidence, result, fixture) {
|
||||
|
||||
@@ -32,50 +32,61 @@ async function runManagedAppManifestSelfTest(context) {
|
||||
const members = inspectManagedMembersBytes(sample.bytes, sample.target);
|
||||
const method = members.methods[0];
|
||||
assert.ok(method);
|
||||
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,
|
||||
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,
|
||||
},
|
||||
methods: [
|
||||
],
|
||||
application_graph: {
|
||||
expected_node_kinds: [
|
||||
"artifact",
|
||||
"managed-assembly",
|
||||
"managed-module",
|
||||
"managed-type",
|
||||
"managed-method",
|
||||
],
|
||||
feature_traces: [
|
||||
{
|
||||
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,
|
||||
label: "trace pinned method by name",
|
||||
method_token: method.token,
|
||||
seed: "PinnedSemanticSlice",
|
||||
match: "exact",
|
||||
min_matched_seeds: 1,
|
||||
},
|
||||
],
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
join(context.workspace, "source-owned-managed-app-manifest.json"),
|
||||
};
|
||||
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,
|
||||
);
|
||||
return {
|
||||
verified: true,
|
||||
rejected_mismatches: rejectedMismatches,
|
||||
assertions: summary.assertions,
|
||||
methods: summary.methods.length,
|
||||
applicationGraph: summary.application_graph ?? null,
|
||||
@@ -83,6 +94,60 @@ 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;
|
||||
|
||||
+45
-28
@@ -779,20 +779,40 @@ if [ "$1" = "version" ]; then ${VERSION_BLOCK}; fi`,
|
||||
});
|
||||
|
||||
it("reads a bounded logcat dump with the caller count and buffer", async () => {
|
||||
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 { 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 result = await evidenceResult(providerOver(binary), {
|
||||
operation: "read_adb_logcat",
|
||||
input: { serial: "emulator-5554", count: 500, buffer: "main" },
|
||||
input: { serial: "emulator-5554", count: 1, buffer: "system", 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(
|
||||
(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).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 () => {
|
||||
@@ -1667,13 +1687,15 @@ exit 1`,
|
||||
operation: "capture_adb_screen",
|
||||
input: { serial: "emulator-5554", output_directory: output },
|
||||
});
|
||||
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);
|
||||
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,
|
||||
);
|
||||
});
|
||||
|
||||
it("stops a screen capture aborted after adb returns but before publication", async () => {
|
||||
@@ -1889,11 +1911,12 @@ 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 result = await evidenceResult(providerOver(binary), {
|
||||
const execution = await providerOver(binary).execute({
|
||||
operation: "inspect_adb_window",
|
||||
input: { serial: "emulator-5554" },
|
||||
});
|
||||
expect(result).toMatchObject({
|
||||
if (!execution.ok) throw execution.error;
|
||||
expect(execution.value.result).toMatchObject({
|
||||
focused_window: "Window{abc u0 com.example/.Main}",
|
||||
focused_app: "ActivityRecord{def u0 com.example/.Main t1}",
|
||||
observed_lines: [
|
||||
@@ -1901,11 +1924,6 @@ 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"),
|
||||
);
|
||||
@@ -1918,16 +1936,15 @@ 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"),
|
||||
);
|
||||
|
||||
@@ -235,16 +235,6 @@ 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")
|
||||
|
||||
@@ -30,7 +30,6 @@ 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 {
|
||||
@@ -337,7 +336,7 @@ const searchClasses = async (
|
||||
request.operation,
|
||||
"JADX class pagination is incomplete",
|
||||
);
|
||||
appendAll(matches, page.items);
|
||||
matches.push(...page.items);
|
||||
offset = page.next_offset;
|
||||
} while (offset < loaded.class_count);
|
||||
const occurrences = new Map<string, number>();
|
||||
|
||||
@@ -24,7 +24,6 @@ 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;
|
||||
@@ -281,7 +280,7 @@ const snapshotCloseFailures = (
|
||||
cause.outcome.kind === "failed" &&
|
||||
cause.outcome.cause instanceof RegularFileCleanupFailure
|
||||
)
|
||||
appendAll(failures, snapshotCloseFailures(cause.outcome.cause));
|
||||
failures.push(...snapshotCloseFailures(cause.outcome.cause));
|
||||
return failures;
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ 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"));
|
||||
@@ -18,9 +17,15 @@ it("names REA_JADX_OPERATION_TIMEOUT_MS when the operation deadline elapses", ()
|
||||
session: undefined,
|
||||
});
|
||||
const projected = projectAnalysisError(present());
|
||||
const guidance = jadxOperationTimeoutGuidance(1500);
|
||||
expect(projected.message).toBe(guidance.userMessage);
|
||||
expect(projected.remediation.action).toBe(guidance.remediationAction);
|
||||
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",
|
||||
);
|
||||
expect(projected.details).toMatchObject({
|
||||
limit: {
|
||||
setting: "jadx.operation_timeout_ms",
|
||||
|
||||
@@ -686,44 +686,20 @@ 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 result = await evidenceResult(providerOver(command), {
|
||||
const execution = await providerOver(command).execute({
|
||||
operation: "decode_android_resources",
|
||||
input: { path: apk.path, include_strings: true },
|
||||
});
|
||||
expect(result.strings).toEqual([]);
|
||||
});
|
||||
|
||||
it("reports apktool's own refusal for undecodable targets", async () => {
|
||||
const { command, root } = await writeApktoolStub(
|
||||
decodeStub({ failDecode: 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",
|
||||
);
|
||||
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 () => {
|
||||
@@ -1525,6 +1501,7 @@ 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();
|
||||
@@ -1677,7 +1654,6 @@ 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"),
|
||||
|
||||
@@ -71,7 +71,6 @@ 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;
|
||||
@@ -658,7 +657,7 @@ export class ApktoolProvider {
|
||||
const projectedStrings = projection.ok
|
||||
? projection.value
|
||||
: projection.error.partial;
|
||||
appendAll(limitations, projectedStrings.limitations);
|
||||
limitations.push(...projectedStrings.limitations);
|
||||
const coverage =
|
||||
projectedStrings.coverage === "partial" ||
|
||||
manifestRead.truncated ||
|
||||
|
||||
@@ -23,7 +23,6 @@ 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" };
|
||||
@@ -137,52 +136,6 @@ 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");
|
||||
@@ -209,6 +162,14 @@ 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" },
|
||||
@@ -246,24 +207,25 @@ describe("navigation procedure admission", () => {
|
||||
},
|
||||
);
|
||||
|
||||
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.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("returns a validated containing procedure name", async () => {
|
||||
const result = await getNavigationContext(
|
||||
|
||||
@@ -97,6 +97,7 @@ 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,
|
||||
@@ -816,6 +817,7 @@ 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);
|
||||
@@ -899,6 +901,7 @@ const isArchiveFormat = (
|
||||
"dfuse",
|
||||
"esp-flash-image",
|
||||
"fit",
|
||||
"ntfs-mft",
|
||||
].includes(format);
|
||||
|
||||
/** Whether the leading bytes begin any executable header family, valid or not. */
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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,
|
||||
@@ -1181,7 +1180,7 @@ const reachableRequirements = (
|
||||
descriptor.runtime_unavailable === true)
|
||||
);
|
||||
});
|
||||
appendAll(reached, available);
|
||||
reached.push(...available);
|
||||
if (available.length === 0) break;
|
||||
}
|
||||
return reached;
|
||||
|
||||
@@ -11,7 +11,6 @@ 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<
|
||||
@@ -73,7 +72,7 @@ export const retryClientConfigurationCleanup = async (
|
||||
let nested: unknown;
|
||||
if (current instanceof RegularFileCleanupFailure) {
|
||||
const retried = await retryRegularFileCleanupFailures(current, resources);
|
||||
appendAll(failures, retried.failures);
|
||||
failures.push(...retried.failures);
|
||||
for (const lease of retried.cleanupLeases) cleanupLeases.add(lease);
|
||||
if (retried.cleanup !== undefined) observations.push(retried.cleanup);
|
||||
primary = retried.primary;
|
||||
|
||||
@@ -389,7 +389,7 @@ const providerSettingSelections = (
|
||||
];
|
||||
|
||||
/** Render what the registration comparison established without inferring its effective environment. */
|
||||
export const registrationEnvironmentDetail = (
|
||||
const registrationEnvironmentDetail = (
|
||||
client: string,
|
||||
{
|
||||
unrecorded,
|
||||
|
||||
@@ -11,7 +11,6 @@ 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 {
|
||||
@@ -33,13 +32,13 @@ export const collectDoctorDiagnostics = async (
|
||||
checks.push(hopperCheck(host, hopperPath));
|
||||
const hopperTimeoutCheck = optionalHopperStartupTimeoutCheck(host);
|
||||
if (hopperTimeoutCheck !== undefined) checks.push(hopperTimeoutCheck);
|
||||
appendAll(checks, runtimeConfigurationChecks(host));
|
||||
checks.push(...runtimeConfigurationChecks(host));
|
||||
const hopperVersionCheck = await optionalHopperVersionCheck(host, hopperPath);
|
||||
if (hopperVersionCheck !== undefined) checks.push(hopperVersionCheck);
|
||||
|
||||
const providerInspections = await host.providerInspections?.(clients ?? []);
|
||||
if (providerInspections !== undefined)
|
||||
appendAll(checks, providerInspections.flatMap(providerDoctorChecks));
|
||||
checks.push(...providerInspections.flatMap(providerDoctorChecks));
|
||||
|
||||
const ilspyCheck = await optionalIlspyCheck(host);
|
||||
if (ilspyCheck !== undefined) checks.push(ilspyCheck);
|
||||
|
||||
@@ -4,12 +4,7 @@ import { expect, it } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import { createDoctorHostFixture as host } from "./Doctor.fixture.js";
|
||||
import {
|
||||
registrationEnvironmentDetail,
|
||||
runDoctor,
|
||||
systemDoctorHost,
|
||||
type DoctorHost,
|
||||
} from "./Doctor.js";
|
||||
import { runDoctor, systemDoctorHost, type DoctorHost } from "./Doctor.js";
|
||||
|
||||
const inspection = (
|
||||
id: string,
|
||||
@@ -186,11 +181,12 @@ it("states each comparison result without a claim it did not establish", async (
|
||||
classification: "unknown",
|
||||
details: { effective_environment: "unknown" },
|
||||
});
|
||||
expect(check?.detail).toBe(
|
||||
registrationEnvironmentDetail("cursor", {
|
||||
unrecorded: [],
|
||||
differing: ["JAVA_HOME"],
|
||||
unresolved: ["GHIDRA_INSTALL_DIR"],
|
||||
}),
|
||||
expect(check?.detail).toContain(
|
||||
"what the server cursor starts receives is unknown",
|
||||
);
|
||||
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");
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveInvestigationEvidenceClosure } from "./investigation/InvestigationEvidence.js";
|
||||
import { analysisEvidenceSources } from "./AnalysisEvidenceSources.js";
|
||||
import { dispatchQueuedInterrupts } from "../process/queuedInterrupts.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
@@ -180,6 +181,17 @@ 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,7 +18,10 @@ import {
|
||||
FINALIZED_PROCESS_CAPTURE_EXAMPLE,
|
||||
} from "../domain/process/processCapture.fixture.js";
|
||||
import { PROCESS_PROVIDER } from "../domain/process/processEvidenceProvider.js";
|
||||
import { parseProcessCapture } from "../domain/process/processCaptureParsing.js";
|
||||
import {
|
||||
parseProcessCapture,
|
||||
safeParseProcessCapture,
|
||||
} from "../domain/process/processCaptureParsing.js";
|
||||
import {
|
||||
digestProcessCommitment,
|
||||
processScenarioSchema,
|
||||
@@ -252,6 +255,10 @@ 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);
|
||||
@@ -259,6 +266,7 @@ 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,6 +9,7 @@ import {
|
||||
proofEvidence,
|
||||
request,
|
||||
reviewedObligation,
|
||||
readyReviewedLedgerRequest,
|
||||
} from "./ReconstructionObligationLedger.fixture.js";
|
||||
import { createEvidence } from "../domain/evidence.js";
|
||||
import type { ReviewedReconstructionObligation } from "../domain/reconstructionObligationLedgerSchemas.js";
|
||||
@@ -282,7 +283,23 @@ describe("reconstruction obligation ledger", () => {
|
||||
});
|
||||
|
||||
describe("reconstruction obligation ledger fail-closed behavior", () => {
|
||||
it("fails closed on parser gaps, ambiguous ownership, and contradiction", () => {
|
||||
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", () => {
|
||||
const original = createEvidence(
|
||||
undefined,
|
||||
{ id: "external", name: "External protocol observer", version: "1" },
|
||||
|
||||
@@ -37,7 +37,6 @@ 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"]>;
|
||||
@@ -211,15 +210,13 @@ const evaluateObligation = (
|
||||
);
|
||||
if (binding !== null)
|
||||
evaluateBinding(candidate, binding, context, diagnostics);
|
||||
appendAll(
|
||||
diagnostics,
|
||||
originalCaseDiagnostics(candidate, originalCases, context.evidence),
|
||||
diagnostics.push(
|
||||
...originalCaseDiagnostics(candidate, originalCases, context.evidence),
|
||||
);
|
||||
evaluateAuthority(candidate, originalCases, context, diagnostics);
|
||||
evaluateDisposition(candidate, diagnostics);
|
||||
appendAll(
|
||||
diagnostics,
|
||||
contradictionDiagnostics(
|
||||
diagnostics.push(
|
||||
...contradictionDiagnostics(
|
||||
candidate.obligation_id,
|
||||
context.contradictions,
|
||||
context.evidence,
|
||||
|
||||
@@ -24,7 +24,6 @@ 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"];
|
||||
@@ -474,9 +473,8 @@ const resolveInternalSpecifier = (
|
||||
// Retain present runtime files first; substitute only a missing counterpart.
|
||||
// Counterparts cover explicit filenames, not directory/index resolution.
|
||||
if (!directoryReference)
|
||||
appendAll(
|
||||
candidates,
|
||||
typescriptEmittedSourceCandidates(fromPath, normalized).map(
|
||||
candidates.push(
|
||||
...typescriptEmittedSourceCandidates(fromPath, normalized).map(
|
||||
({ path }) => path,
|
||||
),
|
||||
);
|
||||
@@ -566,7 +564,7 @@ const appendFile = (
|
||||
limitations: [...(parsed?.limitations ?? [])],
|
||||
});
|
||||
if (parsed === undefined) return;
|
||||
appendAll(output.limitations, parsed.limitations);
|
||||
output.limitations.push(...parsed.limitations);
|
||||
for (const relationship of parsed.relationships) {
|
||||
const { resolutionStrategy, resolutionBasePath, ...graphRelationship } =
|
||||
relationship;
|
||||
@@ -600,7 +598,7 @@ const appendFile = (
|
||||
resolution: resolved.resolution,
|
||||
});
|
||||
}
|
||||
appendAll(output.parseFailures, parsed.parse_failures);
|
||||
output.parseFailures.push(...parsed.parse_failures);
|
||||
};
|
||||
|
||||
const appendReadEntry = (
|
||||
|
||||
@@ -481,7 +481,7 @@ describe("setup plan application", () => {
|
||||
expect(host.doctorCalls).toBe(1);
|
||||
});
|
||||
|
||||
it("treats an empty interactive selection as cancellation", async () => {
|
||||
it("completes an empty interactive selection without changes", async () => {
|
||||
const host = new FakeSetupHost();
|
||||
host.clients = [{ name: "codex", configPath: "/codex.toml" }];
|
||||
|
||||
|
||||
@@ -213,8 +213,6 @@ 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();
|
||||
|
||||
@@ -71,7 +71,6 @@ import {
|
||||
registrationWriteBlocker,
|
||||
type SetupClient,
|
||||
} from "./SupportedClients.js";
|
||||
import { appendAll } from "../domain/arrayAppend.js";
|
||||
|
||||
export class ClientConfigurationCleanupRollbackFailure extends RegularFileCleanupFailure<string> {
|
||||
constructor(
|
||||
@@ -730,9 +729,8 @@ const registrationOwnershipBlocker = (
|
||||
value: parsed.legacyServers[name],
|
||||
paths: [client.configPath],
|
||||
});
|
||||
appendAll(
|
||||
entries,
|
||||
shadowedClientServers(original, parsed, name).legacyServers.map(
|
||||
entries.push(
|
||||
...shadowedClientServers(original, parsed, name).legacyServers.map(
|
||||
(value) => ({ value, paths: [client.configPath] }),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
type SkillFileSnapshot,
|
||||
type SkillPublicationCleanupOwner,
|
||||
} from "./SkillFilePublication.js";
|
||||
import { appendAll } from "../domain/arrayAppend.js";
|
||||
|
||||
export const MANAGED_SKILL_FILES = [
|
||||
"SKILL.md",
|
||||
@@ -282,7 +281,7 @@ export const existingSkillDestinations = async (
|
||||
try {
|
||||
const pathIssues = await skillDirectoryIssues([destination]);
|
||||
if (pathIssues.some(({ kind }) => kind === "unreadable")) {
|
||||
appendAll(issues, pathIssues);
|
||||
issues.push(...pathIssues);
|
||||
continue;
|
||||
}
|
||||
if (pathIssues.length > 0) continue;
|
||||
@@ -488,7 +487,7 @@ const inspectCanonicalSkill = async (
|
||||
const { path: root } = destination;
|
||||
const pathIssues = await skillDirectoryIssues([destination]);
|
||||
if (pathIssues.length > 0) {
|
||||
appendAll(issues, pathIssues);
|
||||
issues.push(...pathIssues);
|
||||
if (failureCause === undefined) failureCause = pathIssues;
|
||||
installedDestination = true;
|
||||
canonical = false;
|
||||
@@ -531,7 +530,7 @@ const inspectCanonicalSkill = async (
|
||||
if (clientIds !== undefined || rootHasFiles || rootIssues.length > 0) {
|
||||
installedDestination ||= rootHasFiles;
|
||||
canonical = canonical && rootCanonical;
|
||||
appendAll(issues, rootIssues);
|
||||
issues.push(...rootIssues);
|
||||
if (!rootCanonical && rootIssues.length === 0)
|
||||
issues.push({
|
||||
client: destination.client,
|
||||
@@ -705,9 +704,8 @@ export const installCanonicalSkill = async (
|
||||
}
|
||||
}
|
||||
const cleanupOwners = [...publications, ...preparedCleanupOwners];
|
||||
appendAll(
|
||||
rollbackFailures,
|
||||
await closeSkillPublications(cleanupOwners, resources),
|
||||
rollbackFailures.push(
|
||||
...(await closeSkillPublications(cleanupOwners, resources)),
|
||||
);
|
||||
if (verified && rollbackFailures.length === 0) return ok("installed");
|
||||
return err(
|
||||
|
||||
@@ -69,7 +69,6 @@ import {
|
||||
redirectedWriteBlocker,
|
||||
type MsixPackageView,
|
||||
} from "./MsixPackageStorage.js";
|
||||
import { appendAll } from "../domain/arrayAppend.js";
|
||||
|
||||
interface ManagedPathStats {
|
||||
readonly uid?: number;
|
||||
@@ -256,7 +255,7 @@ const removeAll = async (
|
||||
];
|
||||
}
|
||||
items.push(await host.removeSkill());
|
||||
if (purgeData) appendAll(items, await host.purgeData());
|
||||
if (purgeData) items.push(...(await host.purgeData()));
|
||||
return items;
|
||||
};
|
||||
|
||||
@@ -1224,24 +1223,17 @@ const purgeSelectedData = async (
|
||||
const owned = names.filter(
|
||||
(name) => !kept.some((entry) => entry.name === name),
|
||||
);
|
||||
return async () => {
|
||||
const removed = await purgeManagedData(root, fileSystem, owned);
|
||||
const result: UninstallItem[] = [];
|
||||
appendAll(result, keptItems);
|
||||
appendAll(result, removed);
|
||||
return result;
|
||||
};
|
||||
return async () => [
|
||||
...keptItems,
|
||||
...(await purgeManagedData(root, fileSystem, owned)),
|
||||
];
|
||||
};
|
||||
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)]));
|
||||
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);
|
||||
items.unshift(...(await reaPlan()));
|
||||
for (const plan of plans) items.push(...(await plan()));
|
||||
return items;
|
||||
};
|
||||
|
||||
@@ -1613,9 +1605,8 @@ const collapseUninstallItems = (
|
||||
for (const next of items) {
|
||||
const { name, actions, ...action } = next;
|
||||
const group = grouped.get(name) ?? [];
|
||||
appendAll(
|
||||
group,
|
||||
actions !== undefined && actions.length > 0 ? actions : [action],
|
||||
group.push(
|
||||
...(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 while retaining the original digest", async () => {
|
||||
it("rejects source text changed across the artifact port before acquiring the resolver", async () => {
|
||||
const artifacts = {
|
||||
...webModuleArtifactsFixture("import(name)"),
|
||||
source: "import(other)",
|
||||
|
||||
@@ -304,22 +304,6 @@ 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())) },
|
||||
|
||||
@@ -725,40 +725,6 @@ 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 { enhancedOutputSchemas } from "../../contracts/toolOutputSchemaGroups.js";
|
||||
import { nativeCallGraphSchema } from "../../domain/native/nativeCallGraph.js";
|
||||
|
||||
it.each(["provider", "workflow"] as const)(
|
||||
"owns nested %s execution data before returning control to callers",
|
||||
@@ -1270,12 +1270,13 @@ it.each([
|
||||
evidenceProfile: revisedProfile,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
if (operation === "get_call_graph")
|
||||
if (operation === "get_call_graph") {
|
||||
expect(nativeCallGraphSchema.safeParse(oldResult).success).toBe(false);
|
||||
expect(
|
||||
enhancedOutputSchemas.get_call_graph.safeParse({
|
||||
normalized_result: oldResult,
|
||||
}).success,
|
||||
).toBe(false);
|
||||
nativeCallGraphSchema.safeParse({ graph: oldResult, limitations: [] })
|
||||
.success,
|
||||
).toBe(true);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ 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,
|
||||
@@ -25,7 +27,7 @@ it("retains both child close failures when a resource owner rejects", async () =
|
||||
}),
|
||||
close: async () => {
|
||||
attempts.push("artifact");
|
||||
throw new Error("artifact close rejected");
|
||||
throw artifactCause;
|
||||
},
|
||||
};
|
||||
const native: AnalysisClient = {
|
||||
@@ -35,14 +37,20 @@ it("retains both child close failures when a resource owner rejects", async () =
|
||||
}),
|
||||
close: async () => {
|
||||
attempts.push("native");
|
||||
throw new Error("native close rejected");
|
||||
throw nativeCause;
|
||||
},
|
||||
};
|
||||
|
||||
const closed = await combineBundleResourceClients(artifact, native).close();
|
||||
expect(attempts.sort()).toEqual(["artifact", "native"]);
|
||||
expect(closed.ok).toBe(false);
|
||||
if (!closed.ok) expect(closed.error.cleanupIncomplete).toBe(true);
|
||||
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);
|
||||
});
|
||||
|
||||
it("reports the first mixed child failure before awaiting the other owner and retries both", async () => {
|
||||
|
||||
@@ -135,6 +135,7 @@ 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,3 +1,4 @@
|
||||
import { parseEvidence } from "../../domain/evidence.js";
|
||||
import { expect, it } from "vitest";
|
||||
import { AppleIpsService } from "./AppleIpsService.js";
|
||||
import { APPLE_IPS_PROVIDER } from "../../domain/investigationProviders.js";
|
||||
@@ -232,12 +233,13 @@ it.each([
|
||||
},
|
||||
);
|
||||
|
||||
it("discards an observation if the caller cancels while the provider completes", async () => {
|
||||
it("retains a completed observation when the caller cancels as the provider completes", async () => {
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "late AppleIps cancellation" };
|
||||
const service = new AppleIpsService({
|
||||
identity,
|
||||
inspect: () => {
|
||||
controller.abort();
|
||||
controller.abort(reason);
|
||||
return Promise.resolve(ok(report));
|
||||
},
|
||||
});
|
||||
@@ -245,8 +247,13 @@ it("discards an observation if the caller cancels while the provider completes",
|
||||
{ path: report.artifact.path },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { parseEvidence } from "../../domain/evidence.js";
|
||||
import { expect, it } from "vitest";
|
||||
import { DevicetreeService } from "./DevicetreeService.js";
|
||||
import { DEVICETREE_PROVIDER } from "../../domain/investigationProviders.js";
|
||||
@@ -72,12 +73,13 @@ it("validates caller paths before invoking the provider", async () => {
|
||||
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisInputError);
|
||||
expect(calls).toBe(0);
|
||||
});
|
||||
it("rejects cancellation at provider completion", async () => {
|
||||
it("retains a completed observation when the caller cancels as the provider completes", async () => {
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "late Devicetree cancellation" };
|
||||
const service = new DevicetreeService({
|
||||
identity: DEVICETREE_PROVIDER,
|
||||
inspect: () => {
|
||||
controller.abort();
|
||||
controller.abort(reason);
|
||||
return Promise.resolve(ok(report()));
|
||||
},
|
||||
});
|
||||
@@ -85,8 +87,13 @@ it("rejects cancellation at provider completion", async () => {
|
||||
{ path: "/board.dtb" },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
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(),
|
||||
});
|
||||
});
|
||||
it("rejects provider coverage and source-range inconsistencies", async () => {
|
||||
for (const change of [
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { parseEvidence } from "../../domain/evidence.js";
|
||||
import { expect, it } from "vitest";
|
||||
import { ElfNotesService } from "./ElfNotesService.js";
|
||||
import { ELF_NOTES_PROVIDER } from "../../domain/investigationProviders.js";
|
||||
@@ -106,12 +107,13 @@ it("retains an examined empty table and rejects an uncounted table", async () =>
|
||||
}
|
||||
});
|
||||
|
||||
it("discards an observation if the caller cancels while the provider completes", async () => {
|
||||
it("retains a completed observation when the caller cancels as the provider completes", async () => {
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "late ElfNotes cancellation" };
|
||||
const service = new ElfNotesService({
|
||||
identity,
|
||||
inspect: () => {
|
||||
controller.abort();
|
||||
controller.abort(reason);
|
||||
return Promise.resolve(ok(report));
|
||||
},
|
||||
});
|
||||
@@ -119,6 +121,11 @@ it("discards an observation if the caller cancels while the provider completes",
|
||||
{ path: report.artifact.path },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
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,
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
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";
|
||||
@@ -406,12 +407,13 @@ it("rejects a report that fails the output schema", async () => {
|
||||
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisOutputError);
|
||||
});
|
||||
|
||||
it("discards an observation if the caller cancels while the provider completes", async () => {
|
||||
it("retains a completed observation when the caller cancels as the provider completes", async () => {
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "late ElfUnwindRanges cancellation" };
|
||||
const service = new ElfUnwindRangesService({
|
||||
identity,
|
||||
inspect: () => {
|
||||
controller.abort();
|
||||
controller.abort(reason);
|
||||
return Promise.resolve(ok(report));
|
||||
},
|
||||
});
|
||||
@@ -419,6 +421,11 @@ it("discards an observation if the caller cancels while the provider completes",
|
||||
{ path: report.artifact.path },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
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,
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { parseEvidence } from "../../domain/evidence.js";
|
||||
import { expect, it } from "vitest";
|
||||
import { NativeInitializersService } from "./NativeInitializersService.js";
|
||||
import { NATIVE_INITIALIZERS_PROVIDER } from "../../domain/investigationProviders.js";
|
||||
@@ -113,12 +114,13 @@ it("rejects a slot offset outside the artifact", async () => {
|
||||
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisOutputError);
|
||||
});
|
||||
|
||||
it("discards an observation if the caller cancels while the provider completes", async () => {
|
||||
it("retains a completed observation when the caller cancels as the provider completes", async () => {
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "late NativeInitializers cancellation" };
|
||||
const service = new NativeInitializersService({
|
||||
identity,
|
||||
inspect: () => {
|
||||
controller.abort();
|
||||
controller.abort(reason);
|
||||
return Promise.resolve(ok(report));
|
||||
},
|
||||
});
|
||||
@@ -126,6 +128,11 @@ it("discards an observation if the caller cancels while the provider completes",
|
||||
{ path: report.artifact.path },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
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,
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
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";
|
||||
@@ -49,12 +50,13 @@ it("rejects a relative path and extra arguments before reading", async () => {
|
||||
expect(calls).toBe(0);
|
||||
});
|
||||
|
||||
it("discards an observation if the caller cancels while the provider completes", async () => {
|
||||
it("retains a completed observation when the caller cancels as the provider completes", async () => {
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "late PeCertificates cancellation" };
|
||||
const service = new PeCertificatesService({
|
||||
identity,
|
||||
inspect: () => {
|
||||
controller.abort();
|
||||
controller.abort(reason);
|
||||
return Promise.resolve(ok(report));
|
||||
},
|
||||
});
|
||||
@@ -62,8 +64,13 @@ it("discards an observation if the caller cancels while the provider completes",
|
||||
{ path: report.artifact.path },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
||||
it("retains complete certificate provenance from the production parser", async () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { parseEvidence } from "../../domain/evidence.js";
|
||||
import { expect, it } from "vitest";
|
||||
import { PeImportsService } from "./PeImportsService.js";
|
||||
import { PE_IMPORTS_PROVIDER } from "../../domain/investigationProviders.js";
|
||||
@@ -116,12 +117,13 @@ it.each(["import_directory", "delay_import_directory"] as const)(
|
||||
},
|
||||
);
|
||||
|
||||
it("discards an observation if the caller cancels while the provider completes", async () => {
|
||||
it("retains a completed observation when the caller cancels as the provider completes", async () => {
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "late PeImports cancellation" };
|
||||
const service = new PeImportsService({
|
||||
identity,
|
||||
inspect: () => {
|
||||
controller.abort();
|
||||
controller.abort(reason);
|
||||
return Promise.resolve(ok(report));
|
||||
},
|
||||
});
|
||||
@@ -129,8 +131,13 @@ it("discards an observation if the caller cancels while the provider completes",
|
||||
{ path: report.artifact.path },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
||||
it("publishes every consumed directory span as its own Evidence location", async () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { parseEvidence } from "../../domain/evidence.js";
|
||||
import { expect, it } from "vitest";
|
||||
import { WindowsShortcutService } from "./WindowsShortcutService.js";
|
||||
import { WINDOWS_SHORTCUT_PROVIDER } from "../../domain/investigationProviders.js";
|
||||
@@ -143,12 +144,13 @@ it("rejects complete coverage that still lists issues", async () => {
|
||||
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisOutputError);
|
||||
});
|
||||
|
||||
it("discards an observation if the caller cancels while the provider completes", async () => {
|
||||
it("retains a completed observation when the caller cancels as the provider completes", async () => {
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "late WindowsShortcut cancellation" };
|
||||
const service = new WindowsShortcutService({
|
||||
identity,
|
||||
inspect: () => {
|
||||
controller.abort();
|
||||
controller.abort(reason);
|
||||
return Promise.resolve(ok(report));
|
||||
},
|
||||
});
|
||||
@@ -156,6 +158,11 @@ it("discards an observation if the caller cancels while the provider completes",
|
||||
{ path: report.artifact.path },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
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,
|
||||
});
|
||||
});
|
||||
Loaded 100 of 570 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user