mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 17:30:32 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
452ed97031 | ||
|
|
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 | ||
|
|
e2585aa7ce | ||
|
|
1d1c7f98ff | ||
|
|
b1f1b3eff3 | ||
|
|
c1d0f2bb45 | ||
|
|
04de5f3200 | ||
|
|
fe791ff159 | ||
|
|
c837575512 | ||
|
|
cdb51058f6 | ||
|
|
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 | ||
|
|
ddb8abf855 |
No files matched your search
@@ -17,9 +17,9 @@ readiness or provider commands.
|
||||
|
||||
Installing this skill supplies instructions; it does not register the REA MCP
|
||||
server, install analysis engines, or add tools to an already-running agent.
|
||||
The exception is a Claude Code, Codex, or Cursor Marketplace plugin install:
|
||||
the plugin bundles this skill and registers its own `rea` server, outside the
|
||||
state that `rea setup` and `rea doctor` manage.
|
||||
The exception is a Claude Code or Codex plugin install: the plugin bundles
|
||||
this skill and registers its own `rea` server, outside the state that
|
||||
`rea setup` and `rea doctor` manage.
|
||||
|
||||
If REA tools are available and their registration is not known to be stale,
|
||||
proceed directly to the target. Do not run diagnostics or setup before every
|
||||
|
||||
@@ -118,22 +118,19 @@ is not a recommendation to change it. Do not invent a setting. Host memory and
|
||||
|
||||
## REA installed as a client plugin
|
||||
|
||||
A Claude Code, Codex, or Cursor Marketplace plugin owns its `rea` server and
|
||||
this skill. Doctor's `--client` and `--skill` checks and `rea setup` read and
|
||||
write only setup-owned state, so for a plugin-only install they report
|
||||
`missing`, and applying setup would add a second `rea` server. If this skill
|
||||
was loaded from a plugin, or the client lists a `rea` plugin, do not prepare a
|
||||
setup plan for missing tools. Instead:
|
||||
A Claude Code or Codex plugin owns its `rea` server and this skill. Doctor's
|
||||
`--client` and `--skill` checks and `rea setup` read and write only
|
||||
setup-owned state, so for a plugin-only install they report `missing`, and
|
||||
applying setup would add a second `rea` server. If this skill was loaded from
|
||||
a plugin, or the client lists a `rea` plugin, do not prepare a setup plan for
|
||||
missing tools. Instead:
|
||||
|
||||
- Check that the plugin is installed and enabled with the client's own
|
||||
listing: `claude plugin list` and `/mcp` in Claude Code, `codex plugin list`
|
||||
in Codex, and Cursor's plugin settings for a Marketplace install.
|
||||
in Codex.
|
||||
- Restart or reconnect the client, then read its launch error for the `rea`
|
||||
server if tools are still absent. Claude Code and Codex start the plugin's
|
||||
bundled runtime; a missing build or version mismatch requires a reinstall,
|
||||
or a fresh build when developing from source. A Cursor Marketplace install
|
||||
starts `npx` at the version pinned in the plugin's `mcp.json`. A failed
|
||||
download needs network access to that published version.
|
||||
server if tools are still absent. The plugin starts its bundled runtime; a missing build or version mismatch
|
||||
requires a reinstall, or a fresh build when developing from source.
|
||||
- Check analysis engines through the connected server's
|
||||
`binary_session.result.tool_availability`. `rea doctor --provider <id> --json`
|
||||
from a shell reports that shell's environment, which can differ from the
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"name": "rea",
|
||||
"version": "6.8.0",
|
||||
"description": "Reverse engineer anything with agents, from app behavior down to native binaries.",
|
||||
"author": {
|
||||
"name": "morluto",
|
||||
"url": "https://github.com/morluto"
|
||||
},
|
||||
"homepage": "https://rea.tools/",
|
||||
"repository": "https://github.com/morluto/rea",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"reverse-engineering",
|
||||
"binary-analysis",
|
||||
"decompiler",
|
||||
"mcp-server"
|
||||
],
|
||||
"skills": ".cursor-plugin/skills",
|
||||
"logo": "assets/rea-plugin.png"
|
||||
}
|
||||
@@ -1,603 +0,0 @@
|
||||
---
|
||||
name: reverse-engineer-anything
|
||||
description: Reverse engineer native, managed, Electron/JavaScript, packaged, firmware, and browser targets with REA, and build agent environments (AgentEnv bundles) of web applications from a live instance or a capture, and of Linux desktop apps with a task from a request. Use shipped-artifact or requested runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
|
||||
metadata:
|
||||
version: "62"
|
||||
---
|
||||
|
||||
# REA
|
||||
|
||||
Use REA when a claim depends on a shipped binary or package, decompilation,
|
||||
requested runtime evidence, or comparison with
|
||||
behavior not established by available source. For ordinary analysis of a
|
||||
complete source repository, use normal repository tools and do not run REA
|
||||
readiness or provider commands.
|
||||
|
||||
## Connect only when needed
|
||||
|
||||
Installing this skill supplies instructions; it does not register the REA MCP
|
||||
server, install analysis engines, or add tools to an already-running agent.
|
||||
The exception is a Claude Code, Codex, or Cursor Marketplace plugin install:
|
||||
the plugin bundles this skill and registers its own `rea` server, outside the
|
||||
state that `rea setup` and `rea doctor` manage.
|
||||
|
||||
If REA tools are available and their registration is not known to be stale,
|
||||
proceed directly to the target. Do not run diagnostics or setup before every
|
||||
investigation. Use the connected server's actual tool list and input schemas;
|
||||
a skill installed from repository main may describe capabilities absent from
|
||||
an older npm release. Keep complete inline Evidence when that server does not
|
||||
advertise retained references.
|
||||
|
||||
If tools are absent, a call is blocked by the client, or a required provider is
|
||||
unavailable, read [connection and recovery](references/connection-and-recovery.md).
|
||||
Diagnose the observed blocker, continue useful supported work, and resume the
|
||||
original task after repair. Client approval, shell PATH, registration and engine
|
||||
availability have different remedies; avoid repeating setup for all of them.
|
||||
If the host times out without a REA result, do not repeat the unchanged call or
|
||||
assume work stopped. Read the recovery guide's deadline distinction before retrying.
|
||||
If the MCP connection drops, retry that call once only when it is safe to
|
||||
repeat or known not to have started. If it may already have changed the
|
||||
target, or it fails again, tell the user and ask them to restart the MCP
|
||||
client. If REA could not start its connection, retry once, then tell the user
|
||||
and ask them to restart the MCP client. Do not retry a startup failure that
|
||||
returns the same unchanged result. For plugin installs, follow
|
||||
[plugin recovery](references/connection-and-recovery.md#rea-installed-as-a-client-plugin).
|
||||
For setup-owned registrations, run `rea doctor` when REA could not start
|
||||
or tools stay missing after that restart. For `cleanup_incomplete`, a retryable
|
||||
capacity limit, or `configuration_invalid`, follow the recovery guide's ladder
|
||||
before asking for a restart. Read that guide before a second repair attempt.
|
||||
|
||||
Choose the installation owner before repairing registration. Plugin installs use
|
||||
[plugin recovery](references/connection-and-recovery.md#rea-installed-as-a-client-plugin);
|
||||
only setup-owned registrations use the guide's
|
||||
[registration repair plan](references/connection-and-recovery.md#repair-registration-when-needed).
|
||||
A plugin-only doctor result of `missing` does not call for `rea setup`.
|
||||
|
||||
REA setup never installs or upgrades Node.js, npm, Homebrew, Java, Ghidra, IDA,
|
||||
JADX, Kuna, Binwalk, or Unblob. Use existing prerequisites; do not install unrelated
|
||||
software to repair MCP registration. For an unsupported client, use manual stdio
|
||||
registration with an existing installation’s absolute runtime and entry script, or continue through the CLI.
|
||||
|
||||
A concrete CLI fallback for an operator-supplied JavaScript tree or ASAR is:
|
||||
|
||||
```bash
|
||||
npx -y rea-agents@latest analyze-javascript-application /absolute/path/to/app --json
|
||||
```
|
||||
|
||||
No MCP registration or native engine is required. Read the returned Evidence,
|
||||
graph, limitations, and unknowns with the same care as an MCP result; the CLI
|
||||
returns the Evidence record directly. This fallback does not establish that
|
||||
MCP is configured. Native CLI tasks still require their selected engine.
|
||||
Prefer an existing `rea` CLI. When none is installed and REA package acquisition
|
||||
is within the authorized scope, use `npx -y rea-agents@latest` in its place from a
|
||||
trusted directory outside the target project, with absolute target paths. Managed
|
||||
setup binds the current installation directly; retain its runtime and package
|
||||
cache entry, or restore that installation before repairing its registration.
|
||||
For setup-owned registration, configuration repair starts with a read-only setup plan; show paths, backups and
|
||||
skill replacement before applying authorized changes. Preserve existing approval
|
||||
and target-execution boundaries.
|
||||
|
||||
## Route the target first
|
||||
|
||||
Choose the first tool from the target the user supplied. Use `open_binary` for
|
||||
active-target native or archive workflows; target-free tools take their own
|
||||
explicit path or endpoint and do not need it.
|
||||
|
||||
- ASAR or extracted JavaScript/Electron tree:
|
||||
`analyze_javascript_application`.
|
||||
- Archive/package member inventory (ZIP/APK/IPA/MSIX/AppX, DMG, or a PyInstaller
|
||||
executable): `open_binary` with the supplied local path; use `inspect_artifact`
|
||||
when its graph and findings help answer the question. A PyInstaller CArchive
|
||||
is the application layer; the bootloader is only the native prefix. Start with
|
||||
`detail: "summary"` for large inventories, then use retained inventory views
|
||||
for selected member fields/pages. A complete small inventory can cost less
|
||||
than extra calls and schemas. Runtime options are metadata, not files.
|
||||
Compact delivery still inventories and hashes members. See the
|
||||
[Python packaged applications guide](https://github.com/morluto/rea/blob/main/docs/python-packaged-applications.md).
|
||||
- Bun `--compile` executable: `open_binary`, then `inspect_artifact`; the Bun
|
||||
module graph is the application layer. Extract stored modules and analyze the
|
||||
JavaScript. See [Bun standalone executables](https://github.com/morluto/rea/blob/main/docs/bun-standalone.md).
|
||||
- Deno `deno compile` executable: `open_binary`, then `inspect_artifact`; the
|
||||
embedded virtual filesystem is the application layer. Extract stored files and
|
||||
analyze compatible JavaScript. Environment-file values and CA certificate bytes
|
||||
are retained in local Evidence with their counts and byte lengths. The npm
|
||||
snapshot length is reported and not decoded, and the executable is not run. See
|
||||
[Deno compile executables](https://github.com/morluto/rea/blob/main/docs/deno-standalone.md).
|
||||
- Apple app plist, Interface Builder, asset catalog, or keyed archive resources:
|
||||
`open_bundle_resources`, then the selected resource tool. This does not
|
||||
require `CFBundleExecutable` or a deep provider. Resource sessions have no
|
||||
executable digest or binary snapshot; use `open_binary` for native code.
|
||||
- Android APK code, classes, methods, or incoming references:
|
||||
`inspect_android_package`, then focused Android tools when advertised.
|
||||
Archive member inventory still uses the archive route above.
|
||||
- Managed PE/CLI assembly: `inspect_managed_artifact`.
|
||||
- Firmware image: `inspect_firmware_regions` when advertised. Use
|
||||
`extract_firmware` when extraction is requested, with a caller-selected new
|
||||
absolute output directory. These tools use caller-supplied Binwalk/Unblob on
|
||||
Linux; see the [firmware guide](https://github.com/morluto/rea/blob/main/docs/guides/firmware/firmware-analysis.md).
|
||||
- ESP32-family app or bootloader image (`0xE9` magic): `inspect_artifact`
|
||||
without a provider; see the ESP section of `references/native-and-artifacts.md`.
|
||||
- .NET NativeAOT PE/ELF: native Ghidra analysis. Native code can yield pseudocode;
|
||||
consult the NativeAOT workflow in `references/native-and-artifacts.md` for
|
||||
optional metadata recovery and its supported host/layout boundary.
|
||||
- Native program behavior, live calls, instrumentation, or command-run effects:
|
||||
choose the matching [native runtime workflow](references/runtime-observation.md#native-runtime-questions).
|
||||
`open_binary` and `trace_native_values` establish static facts; they do not
|
||||
observe execution. Runtime tools have distinct launch, attach, mutation and
|
||||
cleanup effects, and do not require a preliminary static session unless their
|
||||
own contract says so.
|
||||
- User-owned browser page already open: `list_browser_targets`.
|
||||
- Passive traffic capture of a SPA: the default scope is the page's own origin,
|
||||
and data APIs usually live on another origin. Discover API origins from
|
||||
absolute URLs in loaded bundles or script text via JavaScript analysis tools
|
||||
(another passive capture without those origins still scopes to the page
|
||||
origin, and `completeness.excluded` counts lack URLs). Pass both the page
|
||||
origin and those API origins in `allowed_origins` — the list replaces the
|
||||
default scope — and read `completeness.excluded` before reporting that an
|
||||
API is absent. For long captures with body shapes, use
|
||||
`export_evidence_bundle` instead of expecting an inline result.
|
||||
- Browser action the agent must perform (click, type, navigate):
|
||||
`capture_browser_scenario` with declared actions. See
|
||||
[runtime observation](references/runtime-observation.md) for connect/launch
|
||||
and the focused follow-up browser tools.
|
||||
- Agent environment (AgentEnv bundle) of a web application: read
|
||||
[environments](references/environments.md) before recording; it records with
|
||||
`capture_browser_scenario` and writes the bundle with `export_environment_bundle`.
|
||||
- Task for a desktop application's exported environment, from a
|
||||
`capture-desktop-application` folder: read
|
||||
[desktop application tasks](references/environments.md#tasks-for-a-desktop-application-environment).
|
||||
- Environment of a Linux desktop app with a task, from a request such as
|
||||
"build me an Inkscape environment where the agent changes a poster's
|
||||
headline": read [desktop builds](references/desktop-build.md). You drive the
|
||||
app on a build computer from `start_build_computer`, then export and witness.
|
||||
- Fiddler Everywhere traffic or a native `.saz` capture: read the
|
||||
[Fiddler workflow](references/runtime-observation.md#fiddler-everywhere-traffic-debugging).
|
||||
Inspect saved SAZ with `inspect_web_network_capture` and `format: "saz"`;
|
||||
connect Everywhere's separate MCP server for live traffic.
|
||||
- Retained HAR/SAZ/native mitmproxy/aioquic qlog capture: `inspect_web_network_capture`. Select
|
||||
original record ordinals when useful; inspect producer fields and byte/number
|
||||
sidecars without fetching recorded URLs or claiming live attribution. HAR/SAZ/qlog
|
||||
old-generation heap is `REA_CAPTURE_DECODER_OLD_GENERATION_MIB` (default 192 MiB,
|
||||
floor 8, maximum 16384), set where REA starts. Host memory and `NODE_OPTIONS` do not
|
||||
raise it. See
|
||||
[historical captures](https://github.com/morluto/rea/blob/main/docs/guides/web/web-network-captures.md) for the exact upstream
|
||||
profile and credential exclusions. Qlog support starts with aioquic 1.5.0
|
||||
JSON 0.3; stream coverage and endpoint loss declarations do not recover bodies
|
||||
or establish network loss. Prefer detail summary, then kind qlog trace/stream
|
||||
pages; select one trace, stream ID and direction for contribution/range pages.
|
||||
Facts retain all-recorded-events-before-selection scope. Other qlog layouts
|
||||
and PCAP correlation are unsupported.
|
||||
- User-owned Electron runtime already open: `list_electron_targets`. Before an electron-main Inspector attempt, check packaged fuses with `inspect_electron_fuses`; disabled `EnableNodeCliInspectArguments` means the app ignores `--inspect`.
|
||||
- Packet-field discovery: `inspect_packet_fields` searches installed protocols
|
||||
and field names/types before choosing fields for `inspect_packet_capture`.
|
||||
Query/page the relevant records; registrations do not establish observed traffic.
|
||||
- Recorded packet captures: `inspect_packet_capture` with selected TShark fields,
|
||||
display filters and decode-as rules. Requires Linux, util-linux prlimit and
|
||||
TShark 4.x on an absolute `PATH` entry or via the optional absolute
|
||||
`REA_TSHARK_COMMAND` override; see
|
||||
[capture guide](https://github.com/morluto/rea/blob/main/docs/guides/data/packet-captures.md).
|
||||
Packet limits count input packets before filtering. Output defaults to 50 matching
|
||||
frames; use `match_offset` / `match_limit` for later pages, or explicit null
|
||||
limit for complete matching output. Preserve the source digest across pages.
|
||||
This does not perform live capture.
|
||||
For address/DNS/SNI/protocol summaries use `inspect_pcap_capture` with MCP
|
||||
detail summary, then retained kind pcap pages. Requires Wireshark 4.x tshark
|
||||
and capinfos, with Linux prlimit or Windows Job Objects; see
|
||||
[packet summaries](https://github.com/morluto/rea/blob/main/docs/guides/data/pcap-analysis.md).
|
||||
Select an exact DNS question for address pages; page exhaustion does not prove
|
||||
network completeness. This reads recordings and does not acquire traffic.
|
||||
- Decoder discovery: `inspect_signal_decoders` searches installed decoder IDs
|
||||
or selected decoder help, including channels and options. Query and page the
|
||||
relevant details before choosing settings; this does not scan hardware.
|
||||
- Recorded logic/signal traces: `decode_signal_recording` with explicit decoder,
|
||||
channel and protocol settings. On Linux with util-linux prlimit, supply sigrok-cli 0.7.2 through
|
||||
`REA_SIGROK_COMMAND`; see [recording guide](https://github.com/morluto/rea/blob/main/docs/guides/signals/signal-recordings.md).
|
||||
Output defaults to 50 literal lines; select annotation classes and page with
|
||||
`line_offset` / `line_limit`, or explicit null for all remaining output.
|
||||
Window counts are display lines, not annotations or samples. Preserve source digests across pages.
|
||||
This reads existing files; it does not acquire hardware signals.
|
||||
- Explicit local WebAssembly artifact: `inspect_wasm_artifact`. Supply WABT 1.0.42
|
||||
through `REA_WABT_BIN_DIRECTORY`; retain exact byte/tool identity and decoded WAT.
|
||||
Select JavaScript glue and candidate paths explicitly and preserve URL/path
|
||||
ambiguity. No implicit fetch or target execution; WAT is not original source.
|
||||
See the [WASM guide](https://github.com/morluto/rea/blob/main/docs/guides/wasm/wasm-artifacts.md).
|
||||
- Explicit local Android APK (or ZIP/AAB) signing: `inspect_apk_signature`.
|
||||
Prefer its summary or selected signer/digest/lineage page; retain complete
|
||||
Evidence and use `inspect_analysis_view` kind `apk-signature` for follow-ups.
|
||||
Reports which schemes are present (v1 JAR, v2, v3, v3.1), each signer's
|
||||
certificate SHA-256 fingerprint (v1 included, from its PKCS#7 block), v3 key-rotation lineage, source stamp, and
|
||||
whether each signed digest still matches the bytes (self-consistency). This
|
||||
is identity and integrity, not a trust or authenticity verdict.
|
||||
See the [APK signature guide](https://github.com/morluto/rea/blob/main/docs/apk-signature.md).
|
||||
- Explicit local EVM bytecode carrier: `inspect_evm_interface` with caller-selected
|
||||
`raw` or `hex` encoding. The owned worker deadline is `timeout_ms` (default
|
||||
30000 ms, range 1..2147483647, next call). It does not raise the fixed byte,
|
||||
address-space, or CPU allowances. Preserve carrier/decoded digests and treat selectors,
|
||||
argument strings and mutability as inferred candidates; this performs no chain
|
||||
lookup or target execution. For the complete inferred interface without hex or
|
||||
duplicate stdout, discover support and select `detail: "summary"`; complete
|
||||
Evidence stays retained under its original identity. Use the returned
|
||||
`parent_evidence_id` for exact function or `bytecode_hex`/stdout/stderr pages.
|
||||
Candidate coverage does not establish interface completeness. See the
|
||||
[EVM bytecode guide](https://github.com/morluto/rea/blob/main/docs/guides/evm/evm-bytecode.md).
|
||||
- Embedded Go compiler, module, build-setting metadata, or static `(ptr, len)`
|
||||
string literals: `inspect_go_binary` with the explicit local file (pass
|
||||
`include_string_literals: true` and optional `max_string_literals` for bounded
|
||||
inferred static string-header recovery, and page retained rows with
|
||||
`inspect_analysis_view` kind `go` collection `string_literals`). This built-in
|
||||
target-free inspection requires no Go installation or disassembler. Preserve
|
||||
raw module bytes and source ranges; absent metadata does not prove a binary is
|
||||
not Go, static string recovery excludes compiler-inlined stack or immediate
|
||||
strings, and unparsed text records remain explicit. See the
|
||||
[Go binary guide](https://github.com/morluto/rea/blob/main/docs/guides/native/go-binaries.md).
|
||||
- Embedded CUDA/HIP code objects: `inspect_gpu_code_objects`; inventory targets
|
||||
first, then decode selected payloads and query retained GPU Evidence.
|
||||
- Bundled GGUF or safetensors model declarations: `inspect_model_artifact`
|
||||
with the explicit local file. Default header hashing leaves full-file identity
|
||||
unknown; `hash: "full"` additionally reads all weights for SHA-256. MCP returns
|
||||
retained summaries; optional `view` selects one page in the initial call. Use
|
||||
`inspect_analysis_view` kind `model-artifact` for
|
||||
batched metadata keys, tensor names or type totals, selected fields and pages.
|
||||
Preserve header-scoped indices and array preview omissions; do not infer model
|
||||
behavior from declared architecture or tensor types. See the
|
||||
[model file guide](https://github.com/morluto/rea/blob/main/docs/model-artifacts.md).
|
||||
- Unity IL2CPP names, member owners and string literals from `global-metadata.dat`:
|
||||
`inspect_il2cpp_metadata` with the explicit local file; MCP defaults to compact
|
||||
counts. Query sections, images, types, methods, fields, properties, events,
|
||||
parameters or string_literals through `inspect_analysis_view`
|
||||
with the returned `parent_evidence_id`, using exact filters, batch indices and
|
||||
selected fields. Parameters accept `method_indices`, avoiding a scan of all
|
||||
parameter pages. Missing collections in historical Evidence remain unknown.
|
||||
Versions 24, 27, 29, 31 and 39; no Unity, IL2CPP
|
||||
binary or disassembler is run. Unrecognized magic (possibly encrypted) and
|
||||
other versions are unsupported. Optional matching `binary_path` enables static native addresses and type references; query `method_addresses` with indices or addresses/states, preserving every shared-body method and both artifact identities. Without a linked binary native counts remain unknown. Byte-pattern fallback is inferred; no runtime behavior or same-build pairing is proved. See the
|
||||
[IL2CPP metadata guide](https://github.com/morluto/rea/blob/main/docs/il2cpp-metadata.md).
|
||||
- Embedded cargo-auditable Rust dependency metadata: `inspect_rust_binary`
|
||||
with the explicit local ELF, PE, or thin Mach-O file. This built-in
|
||||
target-free inspection requires no Rust installation or disassembler. Preserve
|
||||
package order, dependency indices, and the section file range. A missing
|
||||
`.dep-v0` section is null metadata, not proof the binary has no dependencies.
|
||||
A rustc commit hash is an inference from a read-only `/rustc/<40-hex>/` path.
|
||||
For selected crates, use `view: {collection: "packages", names: [...],
|
||||
fields: ["name", "version", "source"]}` on the first call; explicitly include
|
||||
`dependencies` for direct index references. Use `detail: "summary"` for counts
|
||||
and ranges, or retained `inspect_analysis_view` Rust package/edge pages for
|
||||
subsequent filters. Keep the complete parent Evidence accessible; CLI compact
|
||||
delivery requires `--evidence_output`.
|
||||
See the
|
||||
[Rust binary guide](https://github.com/morluto/rea/blob/main/docs/rust-binaries.md).
|
||||
- PE Authenticode certificate table: `inspect_pe_certificates` with the explicit
|
||||
local PE32/PE32+ file. This built-in target-free inspection requires no
|
||||
Windows SDK. Preserve WIN_CERTIFICATE ranges, types, payload SHA-256, and
|
||||
PKCS#7 signer/X.509 identity fields, including `subject_key_identifier_hex`
|
||||
when the signer uses that form. A missing security directory is observed
|
||||
absence of that table, not proof the image is unsigned in every catalog sense.
|
||||
Signatures are not verified. See the
|
||||
[PE certificate guide](https://github.com/morluto/rea/blob/main/docs/pe-certificates.md).
|
||||
- PE import and delay-load tables: `inspect_pe_imports` with the explicit
|
||||
local PE32/PE32+ file. This built-in target-free inspection requires no
|
||||
Windows SDK. Preserve DLL names, named and ordinal functions, ILT/IAT RVAs,
|
||||
delay-load descriptors, and the pefile import hash of the standard import
|
||||
table. A missing import directory is observed absence of that table, not
|
||||
proof the image never imported APIs through another table. Bound snapshots
|
||||
and loaded IAT contents remain unobserved. See the
|
||||
[PE import guide](https://github.com/morluto/rea/blob/main/docs/pe-imports.md).
|
||||
- Windows Shell Link shortcuts: `inspect_windows_shortcut` with the explicit
|
||||
local `.lnk` file. This built-in target-free inspection requires no Windows
|
||||
SDK or COM. Preserve header flags and FILETIMEs, IDList items, LinkInfo
|
||||
volume and network names, StringData arguments and icon location, ExtraData
|
||||
signatures, and Tracker droid facts labelled derived. Unknown IDList items
|
||||
and ExtraData signatures stay raw with SHA-256. The target path is not
|
||||
resolved or executed. See the
|
||||
[Windows shortcut guide](https://github.com/morluto/rea/blob/main/docs/guides/native/windows-shortcut.md).
|
||||
- PE TLS callbacks and load configuration: `inspect_pe_load_config` with the
|
||||
explicit local PE32/PE32+ file. This built-in target-free inspection requires
|
||||
no Windows SDK. Preserve callback VAs as strings with their RVA, file offset
|
||||
and section, and the size-covered security cookie, SafeSEH, CFG,
|
||||
EH-continuation and XFG fields. Guard-flag mitigation claims are inferred
|
||||
linker declarations, not proof of enforcement. A missing directory is observed
|
||||
absence of that declaration. See the
|
||||
[PE load configuration guide](https://github.com/morluto/rea/blob/main/docs/pe-load-config.md).
|
||||
- ELF, PE, and Mach-O hardening: `inspect_binary_hardening` with the explicit
|
||||
local file. This built-in target-free inspection requires no pwntools or other
|
||||
host tool. One image per Mach-O slice. Each property is `enabled`, `disabled`,
|
||||
`partial`, `unknown`, or `not_applicable`, with the header field or import
|
||||
behind it. A missing stack-protector or fortify import is `unknown`, not
|
||||
proof the protection was omitted. Authenticode and Mach-O code signatures
|
||||
report presence, not validity. Hardened runtime stays unknown and names
|
||||
`inspect_signature`. The image is not executed. See the
|
||||
[binary hardening guide](https://github.com/morluto/rea/blob/main/docs/binary-hardening.md).
|
||||
- ELF note tables: `inspect_elf_notes` with the explicit local ELF32/ELF64
|
||||
file. This built-in target-free inspection requires no disassembler. Preserve
|
||||
PT_NOTE and SHT_NOTE ranges, owners, types, descriptor SHA-256, and GNU
|
||||
build-id or ABI-tag fields. A missing table is observed absence of those
|
||||
declarations, not proof the image never carried notes. See the
|
||||
[ELF notes guide](https://github.com/morluto/rea/blob/main/docs/elf-notes.md).
|
||||
- Code that runs before `main` or at exit in ELF or Mach-O:
|
||||
`inspect_native_initializers` with the explicit local file (and
|
||||
`architecture` for a universal Mach-O). This built-in inspection requires no
|
||||
disassembler. Preserve slot and target addresses, the `resolution` of each
|
||||
entry, and its limitations. An `unresolved` slot is not a zero pointer, and a
|
||||
missing table is not proof that no constructor exists. See the
|
||||
[initializer guide](https://github.com/morluto/rea/blob/main/docs/guides/native/native-initializers.md).
|
||||
|
||||
- ELF unwind function ranges: `inspect_elf_unwind_ranges` with the explicit
|
||||
local ELF32/ELF64 file. This built-in target-free inspection requires no
|
||||
disassembler. Preserve FDE start, end, size, containing section, exact
|
||||
symbol name, CIE index, LSDA, and personality. Page with `offset` and
|
||||
`limit`. A missing `.eh_frame` is observed absence of that declaration,
|
||||
not proof the image has no functions. `.debug_frame` is reported present
|
||||
or absent and is not decoded. CFA instructions are counted and not
|
||||
evaluated. See the
|
||||
[ELF unwind ranges guide](https://github.com/morluto/rea/blob/main/docs/guides/native/elf-unwind-ranges.md).
|
||||
- Apple `.ips` crash reports: `inspect_apple_ips` with the explicit local
|
||||
file. This built-in target-free inspection requires no Xcode, atos, or
|
||||
pwntools. Preserve first-line metadata, `bug_type` as a string, process and
|
||||
exception facts, lossless integer registers, usedImages UUIDs, and
|
||||
documented classification matches. Only `bug_type` `"309"` yields a crash
|
||||
body; other types, including `"288"` stackshots, return no crash result.
|
||||
Opened Mach-O and dSYM UUID joins remain unobserved. See the
|
||||
[Apple IPS guide](https://github.com/morluto/rea/blob/main/docs/apple-ips.md).
|
||||
- Linux kernel module metadata: `inspect_kernel_module` with the explicit local
|
||||
ELF32/ELF64 relocatable file. This built-in target-free inspection requires
|
||||
no Linux host or `modinfo`. Preserve `.modinfo` key order and duplicate keys,
|
||||
`__versions` CRC hex, section file ranges, and the appended signature
|
||||
trailer's PKCS#7 SHA-256. A missing section is observed absence when section
|
||||
names are known, not proof the module never carried that metadata.
|
||||
`struct module` is not decoded. The signature is not validated. gzip, xz, and
|
||||
zstd containers are unsupported; decompress into a separate file with an
|
||||
external codec utility, then inspect the decompressed `ET_REL` image. See the
|
||||
[kernel module guide](https://github.com/morluto/rea/blob/main/docs/kernel-modules.md).
|
||||
- Linux kernel version and build config (`CONFIG_*` options such as
|
||||
`CONFIG_SECURITY_SELINUX`): `inspect_kernel_build_config` with the explicit
|
||||
local `vmlinux`, raw `Image`, x86 `bzImage`, or `configs.ko`. This built-in
|
||||
target-free inspection requires no Linux host or `extract-ikconfig`. Preserve
|
||||
the banner version, config SHA-256, option count, and value kinds; use `name`
|
||||
for one option. An `IKCFG_ST` string is a config only with a valid gzip member
|
||||
and `IKCFG_ED`. An absent config may mean the kernel was built without
|
||||
`CONFIG_IKCONFIG`. Non-gzip payloads are unsupported; decompress externally,
|
||||
then inspect the result. See the
|
||||
[kernel build config guide](https://github.com/morluto/rea/blob/main/docs/guides/native/kernel-build-config.md).
|
||||
- CPython 3.7–3.13 `.pyc` header, nested marshal code objects and bytecode
|
||||
identity: `inspect_python_pyc` with the explicit local file. This built-in
|
||||
target-free inspection requires no Python installation. Preserve magic,
|
||||
PEP 552 hash or mtime/size, nested names, and bytecode SHA-256. Bytecode is
|
||||
not decompiled, and a decoded header does not prove the matching `.py` is
|
||||
present. See the
|
||||
[CPython pyc guide](https://github.com/morluto/rea/blob/main/docs/python-pyc.md).
|
||||
- Erlang BEAM IFF container: `inspect_erlang_beam` with the explicit local
|
||||
`.beam` file. This built-in target-free inspection requires no Erlang
|
||||
installation. Preserve chunk names, atom order, export/import/lambda tables,
|
||||
and the Code header. For selected facts, prefer `detail: "summary"` plus a
|
||||
`beam` view (collection, names/indices, fields and paging), retaining the full
|
||||
Evidence. CLI summary requires `--evidence_output`. Missing table counts stay
|
||||
null; decoded empty tables count zero. Bytecode is not executed. See the
|
||||
[Erlang BEAM guide](https://github.com/morluto/rea/blob/main/docs/erlang-beam.md).
|
||||
|
||||
- Explicit local SQLite database snapshot: `inspect_sqlite_database`. Inspect
|
||||
with Node.js 24.x >=24.15 or 26+ providing native SQLite limits. Read schema
|
||||
first (compact inventory by default), or select an exact ordinary table
|
||||
and `columns` when rows are needed. Defaults are selected-table schema and
|
||||
20 rows; request `schema_scope: "database"` for the complete stored schema. Preserve both database and existing sibling WAL identity; row samples
|
||||
report truncation and have unspecified order. Use a quiescent snapshot rather
|
||||
than assuming a live database/WAL pair was copied atomically. See the
|
||||
[SQLite guide](https://github.com/morluto/rea/blob/main/docs/guides/data/sqlite-databases.md).
|
||||
- Explicit Linux ELF file for offline layout, symbols, relocations or static
|
||||
mitigation evidence: `inspect_binary_layout`. This target-free operation uses
|
||||
caller-supplied pwntools without opening a disassembler database. Preserve its
|
||||
raw locations and inference/coverage limits; see the
|
||||
[offline binary guide](https://github.com/morluto/rea/blob/main/docs/guides/native/binary-diagnostics.md).
|
||||
- Supplied Linux x86-64 ELF core: `inspect_recorded_crash`. Read every recorded
|
||||
thread, raw note and source-bound register without opening a live target.
|
||||
Historical PIDs do not select live processes. Request `include_debugger_context`
|
||||
only when core-only mapping candidates help; see the
|
||||
[recorded crash guide](https://github.com/morluto/rea/blob/main/docs/guides/native/recorded-crashes.md).
|
||||
- PE resource type/name/language trees and static icon group references:
|
||||
`inspect_pe_resources` with an explicit path. This portable operation requires
|
||||
no active target or engine; opaque payloads retain file ranges and SHA-256.
|
||||
- PE export directories and ELF dynamic-symbol exports: `inspect_native_exports`
|
||||
with an explicit path; portable, no active target or engine.
|
||||
- Native executable, library, or analysis database: `open_binary`, then
|
||||
use focused analysis tools directly; call `binary_overview` when metadata or
|
||||
inventory context is useful and available from the selected provider.
|
||||
|
||||
For an existing IDA MCP configuration, use `open_binary` with the original
|
||||
input binary and `provider_id: "ida"`; do not pass an `.idb` or `.i64` database.
|
||||
The legacy attached profile binds the already-open GUI input by SHA-256 and
|
||||
leaves its database open. The modern headless profile creates and closes an
|
||||
owned private database without saving. Installation and registration reuse
|
||||
[mrexodia/ida-pro-mcp](https://github.com/mrexodia/ida-pro-mcp); see the
|
||||
[IDA provider guide](https://github.com/morluto/rea/blob/main/docs/providers/ida-provider.md).
|
||||
For migration to the separate native IDA MCP plugin, follow the guide's
|
||||
[legacy attached migration](https://github.com/morluto/rea/blob/main/docs/providers/ida-provider.md#migrate-a-legacy-attached-registration).
|
||||
Replace the upstream registration explicitly; modern mrexodia tools are not the
|
||||
native profile. Native attachment preserves the database but does not provide
|
||||
direct callers; check session availability and the guide's verification limits.
|
||||
Proceed directly to `analyze_function`, pseudocode, or function/string searches
|
||||
after opening; IDA does not supply `binary_overview`. Consult session availability
|
||||
when composing broader workflows. Modern direct callers and unsupported dossier
|
||||
facets remain unknown; live IDA results are not replayed from snapshots.
|
||||
|
||||
With `REA_KUNA_COMMAND` configured, `provider_id: "kuna"` answers procedure
|
||||
lists, lookups, pseudocode, assembly and `analyze_function` from Kuna's cached
|
||||
whole-program export. Callers/callees use a separate body-free graph shared by
|
||||
the session; `xrefs` returns exact-address incoming sites and filters forwarding
|
||||
aliases. These queries and inventory lookups do not trigger a C export. Indirect
|
||||
targets remain unknown and can make relationship coverage partial. Reference
|
||||
caches expire on close; a fresh CLI process repeats the native reference walk.
|
||||
Typed references, CFG, raw-memory tools and annotations remain unsupported. A cold first
|
||||
answer can come from a single-function run (its limitations say so); assembly
|
||||
waits for the export. For pseudocode and assembly, `raw_result.source` names the
|
||||
cached `.c`/`.asm` file and byte span, which you may search directly instead of
|
||||
calling per function. See the
|
||||
[Kuna provider guide](https://github.com/morluto/rea/blob/main/docs/kuna-provider.md).
|
||||
|
||||
If the user says only "my app" without a name, path, or target already selected
|
||||
for this task, ask which app before searching for artifacts. Paths in unrelated
|
||||
saved reports and nearby test fixtures do not select the user's target. Resolve
|
||||
an actual human-readable app name to one clear installed artifact when possible;
|
||||
ask when matches are ambiguous. Never choose an example app on the user's behalf.
|
||||
|
||||
In a target-free session, use `open_binary` to bind any archive/package or
|
||||
native target whose analysis tool operates on the active target. Do not call an
|
||||
unadvertised tool; inspect `binary_session` with `{}` and its
|
||||
`result.tool_availability` for availability reasons and remediation. Expand each
|
||||
group's `names` into entries, inheriting the inventory-level client requirements
|
||||
unless the group overrides them. The grouped name union is the complete tool
|
||||
inventory; use `tools/list` for canonical tool schemas. The default summary keeps
|
||||
each provider candidate's availability and target-support status, code and
|
||||
reason; `omitted_fields` lists what it leaves out. Request `{"detail": "complete"}`
|
||||
only when you need an unavailable candidate's remediation, provider
|
||||
diagnostics, candidate capability sets, or tool surface, effect and annotation
|
||||
flags; in those closed flag vocabularies, listed
|
||||
flags are true and omitted known flags are false. Provider candidate facts
|
||||
describe possible providers, while top-level session capabilities describe the
|
||||
current route and `analysis_provider_binding` names the selected provider. Do
|
||||
not treat those scopes as interchangeable.
|
||||
|
||||
For editable Ghidra analysis that must survive a later agent process, select
|
||||
the [owned Program checkpoint workflow](references/native-and-artifacts.md#continue-editable-ghidra-analysis-across-processes)
|
||||
before making metadata edits. It is separate from Evidence/Unknown persistence.
|
||||
|
||||
For a Ghidra startup timeout on a large native target, follow
|
||||
[deadline recovery](references/native-and-artifacts.md#ghidra-startup-deadline-recovery)
|
||||
before abandoning deep analysis or repeating the same cold import. The default
|
||||
330 seconds can be changed for new startup attempts through
|
||||
`update_analysis_settings` using `ghidra.startup_timeout_ms`. Changing the
|
||||
server's launch environment instead requires a new REA process.
|
||||
|
||||
## Work summary-first
|
||||
|
||||
Start with the default result and use its inline Evidence and graph context.
|
||||
For a JavaScript application or ELF layout whose size is unknown or large,
|
||||
call `analyze_javascript_application` or `inspect_binary_layout` with
|
||||
`"detail": "summary"`, then inspect the retained `parent_evidence_id` with
|
||||
`inspect_analysis_view`. Over MCP, `analyze_function` is summary-first without
|
||||
a flag: its default reply is the summary, and the complete dossier stays
|
||||
retained under `parent_evidence_id` for facet paging. Pass that ID as a
|
||||
`retained-evidence` reference to `compare_functions`, or inside
|
||||
`match_native_functions` `left_observations`/`right_observations` or
|
||||
`build_call_path.functions`; request `"detail": "complete"` only when the
|
||||
dossier must travel inline.
|
||||
Use the same exact-reference form in other advertised logical Evidence inputs,
|
||||
including optional sources, arrays and nested sources across domains. Each
|
||||
source can independently be inline or retained; preserve its source identity,
|
||||
coverage, partial facts and unknowns. A summary or inspection page has its own
|
||||
Evidence ID: use `normalized_result.parent_evidence_id` when the consumer needs
|
||||
the complete source. For portable CLI follow-ups, commands advertising
|
||||
`--evidence-bundle` resolve references from that selected exported bundle.
|
||||
See [Evidence composition](references/evidence-workflows.md) for scope and
|
||||
recovery; file selectors are supported only where explicitly advertised.
|
||||
Page module identities and select the returned `node_id` before requesting an
|
||||
item; a single file path can represent several graph roles. Do not repeat an
|
||||
identical analysis or rejected call without a changed input or resolved blocker.
|
||||
Recheck session state when a lifecycle change makes that status useful.
|
||||
Make a focused follow-up only when the
|
||||
returned result leaves a specific question unanswered.
|
||||
|
||||
If MCP reports `resource_constraint` with `details.resource: "transport"`, call
|
||||
`inspect_analysis_view` with its reported same-session Evidence reference for a
|
||||
summary, one section/module, or a stable page. Use `trace_application_feature`
|
||||
once a seed is known, or call `export_evidence_bundle` to write the complete
|
||||
session to a selected path. The analysis remains complete in the ledger; avoid
|
||||
repeating it merely to request the same oversized response. Complete CLI JSON
|
||||
output also streams without a single MCP frame.
|
||||
|
||||
When the retained parent ID is unknown, use `get_evidence_bundle` with
|
||||
`detail: "summary"` and exact filters for operation, target SHA-256,
|
||||
analysis-profile digest or canonical procedure address. Select its returned
|
||||
Evidence ID for the next view instead of requesting the complete bundle. When
|
||||
that record has no supported view (for example an annotation), read it with
|
||||
`detail: "records"` and an `evidence_id` filter; `missing_evidence_links`
|
||||
names linked sources this session does not retain.
|
||||
`retention: "complete-record"` means the whole record is stored; native
|
||||
value-flow truncation and unknown metadata remain separate. For an exported
|
||||
portable bundle, use `rea inspect-evidence-bundle` with JSON containing `path`,
|
||||
`detail: "summary"` and the same `filters`. Reading that file still requires
|
||||
validating its full contents within the local memory budget.
|
||||
|
||||
A client can also truncate a successful tool result without a REA transport
|
||||
error. Treat that preview as incomplete; use the retained parent ID for smaller
|
||||
views or export when the full record is needed. Do not parse the preview as
|
||||
canonical JSON or repeat the producer. For JavaScript, read the
|
||||
[module-view and trace recipes](references/javascript-applications.md) before
|
||||
drilling into a large graph; a broad bidirectional trace can expand unrelated
|
||||
containment relationships even after summary analysis.
|
||||
|
||||
Every conclusion must distinguish observations, inferences, and unknowns. Cite
|
||||
Evidence IDs, preserve limitations and incomplete coverage, and never imply
|
||||
that static analysis observed execution. Runtime requests execute the declared
|
||||
target and lifecycle; do not broaden the target or action beyond those fields.
|
||||
|
||||
Within the user's requested investigation, call available analysis tools
|
||||
directly. REA does not require permission grants or per-call approval flags.
|
||||
The agent client can independently require approval. Follow the declared request
|
||||
scope and the host's actual access requirements; use the recovery guide for an
|
||||
observed client-policy rejection.
|
||||
|
||||
## Plan broader investigations
|
||||
|
||||
For requests that span multiple features or subsystems, use a staged workflow:
|
||||
|
||||
1. Turn the request into a checklist of questions and the evidence each answer
|
||||
needs. Resolve target identity and constraints from the conversation and
|
||||
workspace before asking for information again.
|
||||
2. Inspect the current REA session, artifact identity, saved analysis database,
|
||||
bookmarks, and prior evidence. Reuse matching state; do not open duplicate
|
||||
sessions or repeat identical analysis.
|
||||
3. Start with the smallest useful overview or inventory. Follow each question
|
||||
from its entry point through relevant data and state changes to its result.
|
||||
Batch related operations around a specific hypothesis, then expand only when
|
||||
the returned evidence leaves a concrete gap.
|
||||
4. Inspect relevant packaged resources and configuration alongside code when
|
||||
they affect the question. Use format-aware inventory and parsers; do not
|
||||
infer behavior from filenames, strings, or layout alone.
|
||||
5. Corroborate a conclusion with the evidence type it requires. Use runtime
|
||||
observation only when static evidence cannot answer the question and the
|
||||
required host runtime and OS access are available. For JavaScript behavior,
|
||||
run probes against the actual app through the available browser, Electron,
|
||||
or process capture workflow.
|
||||
6. Decompose work into independent questions. When parallel workers are
|
||||
available and the questions do not depend on one another, assign distinct
|
||||
scopes, point workers to existing evidence, and ask them to return sources,
|
||||
conclusions, and unresolved gaps. Otherwise, work sequentially.
|
||||
7. Keep a concise finding ledger linking each conclusion to Evidence IDs,
|
||||
confidence/evidence type, search boundary, and remaining unknowns. Update
|
||||
the shared index or investigation report so later passes can reuse results.
|
||||
|
||||
Before finishing, revisit the original checklist. Mark each question as
|
||||
answered, partially answered, or unresolved based on its evidence; keep
|
||||
bounded negative searches bounded, and do not describe a broad investigation
|
||||
as complete while required questions remain open.
|
||||
|
||||
## Read only the relevant guide
|
||||
|
||||
- Native binaries, managed assemblies, archives, and extraction:
|
||||
[references/native-and-artifacts.md](references/native-and-artifacts.md)
|
||||
- ASARs, extracted JavaScript, feature tracing, and version comparison:
|
||||
[references/javascript-applications.md](references/javascript-applications.md)
|
||||
- Android APK declarations, classes, methods, and static references:
|
||||
[references/android-applications.md](references/android-applications.md)
|
||||
- Native instrumentation, debugger and process capture; browser/Electron
|
||||
observation; static/runtime comparison:
|
||||
[references/runtime-observation.md](references/runtime-observation.md)
|
||||
- Evidence paging, comparisons, residual unknowns, and verification:
|
||||
[references/evidence-workflows.md](references/evidence-workflows.md)
|
||||
- Agent environments (AgentEnv bundles) of a web application, recorded from
|
||||
a live instance or a HAR:
|
||||
[references/environments.md](references/environments.md)
|
||||
- Tasks for a desktop application's exported environment:
|
||||
[references/environments.md](references/environments.md#tasks-for-a-desktop-application-environment)
|
||||
- A desktop app environment from a request, built and witnessed on build
|
||||
computers: [references/desktop-build.md](references/desktop-build.md)
|
||||
|
||||
## Finish the task
|
||||
|
||||
Explain findings in plain language and tie them to returned evidence. When the
|
||||
user asks to build something, use normal coding tools and separate observed
|
||||
behavior from design choices. Close an opened native session with
|
||||
`close_binary` when its engine is no longer needed. Evidence references and Unknown
|
||||
history remain in memory on the same MCP connection. End the investigation with
|
||||
`reset_investigation`; supply `save_path` to checkpoint the complete bundle before
|
||||
clearing. Import the saved bundle to restore original references and Unknown
|
||||
history in a new process. Retention is bounded and is not automatic persistence.
|
||||
@@ -1,63 +0,0 @@
|
||||
# Android application artifacts
|
||||
|
||||
Use the connected server's advertised Android tools. Repository main and npm
|
||||
4.1.0 include this family. A skill installation alone cannot add it to an
|
||||
older server.
|
||||
|
||||
Start with `inspect_android_package` on the caller's APK path for package
|
||||
identity, manifest declarations, coverage, and inline Evidence. Use
|
||||
`search_android_classes` to find candidates, `inspect_android_class` for one
|
||||
class's members, `inspect_android_method` for decompilation, and
|
||||
`trace_android_references` for incoming static references. Each call takes its
|
||||
own APK path and explicit selectors; do not open a native session first.
|
||||
Choose an `overload_index` from the class inventory when methods are ambiguous
|
||||
instead of guessing.
|
||||
|
||||
For JNI implementations, discover `inspect_android_jni_bindings`. Select raw
|
||||
DEX class/method identities and optional descriptors, ABI or logical library
|
||||
paths. Prefer `detail: "summary"`; complete Evidence remains retained and
|
||||
`android-jni` views page declarations, libraries, candidates or binding rows
|
||||
with exact fields. Global native-pair ambiguity survives class selection.
|
||||
Preserve slot/relocation provenance and source coverage: matched means observed
|
||||
candidate presence; partial scans do not establish uniqueness or absence.
|
||||
This reads DEX/ELF bytes without loading the APK into the decompiler or executing
|
||||
target code. Runtime registration and loading remain unknown. See the
|
||||
[JNI guide](https://github.com/morluto/rea/blob/main/docs/guides/android/android-analysis.md#link-native-declarations-to-jni-candidates).
|
||||
|
||||
For an R8/ProGuard release build with its `mapping.txt`, pass the absolute
|
||||
`mapping_path` to `inspect_android_class` or `inspect_android_method`. Prefer
|
||||
`detail: "summary"` to receive normalized symbols or the selected body without
|
||||
duplicate raw class metadata. Complete Evidence stays retained under the linked
|
||||
parent ID and can be exported; CLI summary delivery requires `--evidence_output`.
|
||||
Omitted or complete detail preserves the full inline response. Keep
|
||||
selecting by observed names; the separate `mapping` block reports each symbol
|
||||
as `mapped`, `ambiguous` or `unmapped` after checking DEX types and prototypes.
|
||||
REA does not verify that the mapping belongs to the APK build.
|
||||
|
||||
The provider requires a separately supplied headless JADX MCP JAR 0.7.1 and
|
||||
Java 17 or newer. `REA_JADX_MCP_JAR` selects the absolute JAR path; `JAVA_HOME`
|
||||
can select Java. Use `rea doctor --provider jadx --json` to check the JADX
|
||||
configuration and obtain remediation for missing prerequisites. Java and the
|
||||
headless JADX MCP JAR remain caller-supplied; REA setup does not install or
|
||||
configure them. Configuration readiness does not establish successful analysis
|
||||
of a particular APK.
|
||||
|
||||
Linux and macOS use the POSIX owned-process boundary. Windows x64 requires the
|
||||
matching bundled native controls and uses an owned Job Object with bidirectional
|
||||
stdio. A full JDK, including `jdk.compiler`, is required on every host; Windows
|
||||
`JAVA_HOME` selects `bin\java.exe`. Consult the Android guide for actual host
|
||||
verification coverage. Calls use one worker and provider deadlines; optional
|
||||
`REA_JADX_HEAP_MIB` selects the JVM heap and `REA_JADX_OPERATION_TIMEOUT_MS`
|
||||
the per-operation deadline (integer milliseconds from 1 to 2147483647, default
|
||||
120000). To change that deadline on a running MCP server, set
|
||||
`jadx.operation_timeout_ms` through `update_analysis_settings`, then retry after
|
||||
work and cleanup settle. Newly accepted operations use the override; accepted
|
||||
work keeps its budget. Changing the launch environment requires a new process.
|
||||
The MCP client's tool timeout must also allow the selected deadline. The APK is never launched,
|
||||
and no emulator is required.
|
||||
|
||||
Manifest entries and decompiled text are static observations, not execution or
|
||||
policy enforcement. Incoming references are recovered static relationships;
|
||||
reflection, dynamic loading, and unresolved calls remain unknown. Preserve
|
||||
artifact digests, locations, diagnostics, and incomplete coverage. For archive
|
||||
members rather than code analysis, use `open_binary` and `inspect_artifact`.
|
||||
-252
@@ -1,252 +0,0 @@
|
||||
# Connection and task recovery
|
||||
|
||||
Read this guide only for an observed connection, command, approval or provider
|
||||
blocker. Keep the user's target and question in view. Explain what prevented the
|
||||
operation and the next useful step; after repair, continue the investigation.
|
||||
|
||||
Commands below use an installed `rea`. If none is available, use the package
|
||||
invocation in `SKILL.md` when acquiring REA is authorized. Follow the connected
|
||||
server's schemas and the installed CLI's help rather than assuming repository
|
||||
main and an older release expose the same arguments. Tool-name prefixes belong
|
||||
to the client: find the canonical REA operation in its actual tool listing and
|
||||
server association before concluding that it is absent. A shared prefix alone
|
||||
does not establish which server an approval wildcard covers. See
|
||||
[client tool names and discovery](https://github.com/morluto/rea/blob/main/docs/getting-started/client-configuration.md#client-tool-names-and-discovery).
|
||||
|
||||
## Choose the remedy from the observed failure
|
||||
|
||||
| Observation | Meaning and next step |
|
||||
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| No REA tools in the active session | If REA was installed as a client plugin, follow the plugin section below. Otherwise inspect registration with `rea doctor --client codex --json`. A skill installation alone does not register MCP. |
|
||||
| Missing, malformed or stale registration | Prepare a scoped setup plan; apply only authorized changes with backups. |
|
||||
| Aligned registration, but no connected tools | Restart/reconnect the client. Doctor checks registration; it cannot establish the current agent's connection. Inspect the client's launch error if reconnection fails. |
|
||||
| MCP connection dropped or REA could not start | If the connection drops, retry that call once only when it is safe to repeat or known not to have started. If it may already have changed the target, or the same failure returns, tell the user REA's connection failed and ask them to restart the MCP client. If REA could not start its connection, retry once, then tell the user and ask them to restart the MCP client. Do not retry a startup failure that returns the same unchanged result. Run `rea doctor` when REA could not start or tools stay missing after restart. Doctor cannot restore a live connection by itself. |
|
||||
| `MCP tool call requires approval, but approval policy is never` | Codex rejected the call before REA executed it. Do not install an engine or repeat setup to solve this client-policy decision. See the approval section below. |
|
||||
| REA returns `provider_unavailable` or an unavailable tool entry | Read that operation's provider diagnostics or `binary_session.result.tool_availability`. Expand each group's tool names and inherit the shared client requirements unless overridden. Use the operation's availability, reason, and remediation; a complete tool catalog does not imply every operation is available. Request `{"detail": "complete"}` only for provider diagnostics. |
|
||||
| `rea` is missing in a child shell, or Node is incompatible | Inspect the actual shell PATH and launcher/runtime. A prefix installation may be absent from a login shell's PATH; this does not establish a broken MCP registration. |
|
||||
|
||||
If the client cannot launch its shell/sandbox helper or read the installed skill,
|
||||
inspect its launch diagnostic. For example, Codex can refuse helper aliases when
|
||||
CODEX_HOME is under `/tmp`; use an authorized profile outside the OS temporary
|
||||
directory. REA setup cannot repair the client's executor. Preserve the sandbox
|
||||
instead of silently disabling it to make an analysis command work.
|
||||
|
||||
Static JavaScript and ordinary ZIP inventory need neither Hopper nor Ghidra.
|
||||
HAR inspection is offline and does not require a live browser. Native
|
||||
decompilation, Android analysis and firmware extraction have their own
|
||||
bring-your-own prerequisites. Use supported inventory/static evidence when it
|
||||
answers part of the question, and name the unavailable evidence separately.
|
||||
Do not substitute another target. Label observations from ordinary shell tools
|
||||
as such; only actual REA output supplies REA Evidence.
|
||||
|
||||
## Distinguish client and server deadlines
|
||||
|
||||
A host message such as `timed out awaiting tools/call` with no REA result means
|
||||
the client's deadline expired before a result arrived. It does not establish
|
||||
that REA finished, canceled successfully, or released all resources. Do not
|
||||
repeat the unchanged analysis. Inspect available session/provider state and
|
||||
retained Evidence only when useful; an idle `binary_session` is not proof that
|
||||
every target-free operation stopped. Never invent a missing Evidence reference.
|
||||
|
||||
Allow the host tool deadline to cover execution, cancellation/cleanup, and
|
||||
response delivery. In Codex, the setting is
|
||||
`mcp_servers.<server-id>.tool_timeout_sec` in **seconds**, not
|
||||
`startup_timeout_sec`. Select a value for the actual operation budget and
|
||||
headroom, not a universal timeout. See the
|
||||
[official MCP configuration reference](https://learn.chatgpt.com/docs/extend/mcp?surface=cli).
|
||||
Apply configuration changes only within the authorized scope, preserving
|
||||
unrelated entries and backups; reconnect/restart as required by the host.
|
||||
When configuration is not editable, report the setting needed or use an
|
||||
authorized CLI fallback. Do not silently broaden permissions or switch targets.
|
||||
|
||||
A returned REA `provider_timeout` instead means the **server/provider** reached
|
||||
its deadline. Follow its `remediation.action`, reuse accessible partial Evidence,
|
||||
and resolve reported incomplete cleanup before retrying. Change a request
|
||||
field or a `REA_*` setting only when the returned remediation explicitly
|
||||
recommends that change. Restart REA when that text says the value is read at
|
||||
startup. A mention that a control does not raise the deadline is not a
|
||||
recommendation to change it. Do not invent a setting the remediation does not
|
||||
name. Raising only the client deadline does not change
|
||||
this server budget. For static JavaScript,
|
||||
`analysis_timeout_ms` (CLI `rea analyze-javascript-application <path>
|
||||
--analysis-timeout-ms <milliseconds>`) controls each source's
|
||||
startup, analysis and transfer deadline in **milliseconds**. Increase that
|
||||
budget or reduce semantic work / select exact inventoried `source_paths` when
|
||||
the task permits. Omit `source_paths` for all sources; `[]` is invalid.
|
||||
`detail: "summary"` reduces delivery, not analysis work. A per-source budget
|
||||
does not bound the elapsed time for an entire multi-source application.
|
||||
Partial-Evidence inspection can avoid recomputation; an unavailable reader is
|
||||
not itself a reason to block a corrected static-analysis retry.
|
||||
The shorthand `rea analyze <path>` does not expose this timeout flag; use the
|
||||
explicit JavaScript command when changing that budget.
|
||||
|
||||
For invalid arguments, use the connected tool's current input schema, or the
|
||||
installed CLI's command help. Refresh the catalog if stale; do not guess fields
|
||||
from an older skill or return the entire catalog to repair one field. Correct
|
||||
all reported fields before retrying.
|
||||
|
||||
## Recovery ladder
|
||||
|
||||
Follow the returned remediation. Change a request field or a `REA_*` setting
|
||||
only when that text explicitly recommends the change. Restart REA when it says
|
||||
the value is read at startup. A mention that a control does not raise a limit
|
||||
is not a recommendation to change it. Do not invent a setting. Host memory and
|
||||
`NODE_OPTIONS` do not raise a REA admission unless that remediation says so.
|
||||
|
||||
- MCP connection dropped: retry that call once only when it is safe to repeat
|
||||
or known not to have started. If it may already have changed the target, or
|
||||
it fails again, tell the user REA's connection failed and ask them to restart
|
||||
the MCP client. Do not run `rea doctor` for the drop itself.
|
||||
- REA could not start its connection: retry once, then tell the user and ask
|
||||
them to restart the MCP client. Run `rea doctor` when REA could not start or
|
||||
tools stay missing after that restart. Do not retry a startup failure that
|
||||
returns the same unchanged result.
|
||||
- `cleanup_incomplete`, or retained cleanup owners in the error details: retry
|
||||
that exact cleanup owner. Do not start new work that needs those resources
|
||||
until that owner confirms release. Repeating the original call is not that
|
||||
retry.
|
||||
- Capacity, when the result is `resource_constraint` and `retryable` is true:
|
||||
wait for the active admission to finish, then retry within the reported
|
||||
limits. Do not restart REA to free the slot.
|
||||
- `configuration_invalid`: correct the named setting in the environment that
|
||||
starts REA (the shell or the MCP registration), then restart REA. Run
|
||||
`rea doctor` to see the `runtime-configuration:` check. Re-run `rea setup`
|
||||
for that client when the registration must carry the new setting, then
|
||||
reconnect.
|
||||
- `invalid_request`: correct the reported fields from the current schema. Do
|
||||
not repeat the unchanged call.
|
||||
|
||||
## REA installed as a client plugin
|
||||
|
||||
A Claude Code, Codex, or Cursor Marketplace plugin owns its `rea` server and
|
||||
this skill. Doctor's `--client` and `--skill` checks and `rea setup` read and
|
||||
write only setup-owned state, so for a plugin-only install they report
|
||||
`missing`, and applying setup would add a second `rea` server. If this skill
|
||||
was loaded from a plugin, or the client lists a `rea` plugin, do not prepare a
|
||||
setup plan for missing tools. Instead:
|
||||
|
||||
- Check that the plugin is installed and enabled with the client's own
|
||||
listing: `claude plugin list` and `/mcp` in Claude Code, `codex plugin list`
|
||||
in Codex, and Cursor's plugin settings for a Marketplace install.
|
||||
- Restart or reconnect the client, then read its launch error for the `rea`
|
||||
server if tools are still absent. Claude Code and Codex start the plugin's
|
||||
bundled runtime; a missing build or version mismatch requires a reinstall,
|
||||
or a fresh build when developing from source. A Cursor Marketplace install
|
||||
starts `npx` at the version pinned in the plugin's `mcp.json`. A failed
|
||||
download needs network access to that published version.
|
||||
- Check analysis engines through the connected server's
|
||||
`binary_session.result.tool_availability`. `rea doctor --provider <id> --json`
|
||||
from a shell reports that shell's environment, which can differ from the
|
||||
environment the client gave the plugin server. If you still run it through
|
||||
the package runner, use the version pinned in the plugin manifest's server
|
||||
entry (`rea-agents@<version>`), not `@latest`, so it matches the server the
|
||||
plugin starts.
|
||||
|
||||
Use the setup repair below only when the user wants a setup-owned registration
|
||||
instead of the plugin, and remove or disable the plugin first.
|
||||
|
||||
## Repair registration when needed
|
||||
|
||||
For Codex:
|
||||
|
||||
```bash
|
||||
rea doctor --client codex --json
|
||||
rea setup --client codex --dry-run --json
|
||||
```
|
||||
|
||||
Use the current supported client's ID: `claude_code`, `claude_desktop`, `codex`,
|
||||
`cursor`, `gemini_cli`, `windsurf`, `devin`, `opencode`, `antigravity`,
|
||||
`copilot_cli`, `commandcode`, `qwen_code`, `vscode`, `trae`, `grok_build`, `omp`, `pi`,
|
||||
`hermes`, `qoder`, `droid`, `zcode`, `dsh`, `freebuff`, or `grok_bot`. If unknown, inspect `doctor --json` before choosing a
|
||||
scope. `grok_bot` uses a chat registration step rather than local `mcp.json`.
|
||||
For unsupported clients, use manual stdio registration with the selected
|
||||
installation’s absolute runtime and entry script, or continue through the CLI.
|
||||
Acquire packages from a trusted directory outside the target project. Keep a
|
||||
package-runner installation and its runtime available while registrations use it.
|
||||
|
||||
Show the plan's exact paths, backups, changes and bundled-skill replacement.
|
||||
Obtain approval for writes when that concrete scope is not already authorized,
|
||||
then apply the same scope with `rea setup --client codex --yes`. Add
|
||||
`--install-hopper` only when separately needed and explicitly approved. Do not
|
||||
install or upgrade unrelated prerequisites. Restart/reconnect, verify tools
|
||||
actually appear, and resume the original task. If they remain absent, inspect
|
||||
the launch failure rather than looping through setup.
|
||||
|
||||
## Codex tool approval is separate from REA readiness
|
||||
|
||||
REA tools have no permission-grant handshake. Codex can still require approval
|
||||
because an operation creates owned resources, launches a helper or has cleanup
|
||||
effects. An advertised tool or successful doctor result does not override that
|
||||
client policy. `workspace-write` alone does not resolve an MCP approval
|
||||
rejection, and `approval_policy = "never"` does not mean every MCP tool is
|
||||
preapproved.
|
||||
|
||||
When interactive approval is available, use the client's approval flow for the
|
||||
requested operation. In an unattended run, continue through an authorized CLI
|
||||
workflow if the execution environment permits it, or report the blocked
|
||||
operation and the approval needed to proceed. Do not retry the same rejected
|
||||
call, silently broaden approvals, or rewrite MCP effects as read-only.
|
||||
|
||||
For an operator who explicitly wants to preapprove named operations in a
|
||||
disposable Codex profile, current Codex supports per-tool configuration. For
|
||||
example, opening and closing an analysis session can be scoped independently:
|
||||
|
||||
```toml
|
||||
[mcp_servers.rea.tools.open_binary]
|
||||
approval_mode = "approve"
|
||||
|
||||
[mcp_servers.rea.tools.close_binary]
|
||||
approval_mode = "approve"
|
||||
```
|
||||
|
||||
Use the actual server ID and only the operations whose effects were approved;
|
||||
this example does not approve inspection, extraction or runtime execution.
|
||||
Show the proposed config edit and preserve unrelated entries and backups before
|
||||
writing. Managed policy can constrain client settings. Consult the
|
||||
[Codex configuration reference](https://learn.chatgpt.com/docs/config-file/config-reference)
|
||||
for the installed client's supported settings. This is an optional recovery
|
||||
choice, not a prerequisite for every REA task.
|
||||
|
||||
## Repair an editable REA limit
|
||||
|
||||
If `details.limit` names an editable setting and its bounds, call
|
||||
`update_analysis_settings` directly with a `changes` entry containing `setting`,
|
||||
`action: "set"` and `value`. `inspect_analysis_settings` is optional when those
|
||||
facts are already present. Retry the same input after work and cleanup settle.
|
||||
Runtime defaults affect new work only; explicit per-call arguments take
|
||||
precedence and must be corrected directly when selected. Reset restores launch
|
||||
values. Updates survive target close/open but disappear on REA restart. They do
|
||||
not edit host deadlines, response buffers, registrations or fixed limits.
|
||||
|
||||
## Continue through an installed CLI
|
||||
|
||||
Check the command environment only if the launcher fails:
|
||||
|
||||
```bash
|
||||
command -v rea
|
||||
command -v node
|
||||
node --version
|
||||
rea --version
|
||||
```
|
||||
|
||||
Use the public launcher at its known installation path with a supported existing
|
||||
Node on PATH when the child shell loses a prefix installation. Avoid internal
|
||||
`dist/cli.js` entrypoints or global toolchain changes. MCP may have its own valid
|
||||
launcher even when a shell cannot resolve `rea`.
|
||||
|
||||
For a user-supplied JavaScript tree or ASAR, select output files in the authorized
|
||||
workspace and save complete Evidence before reading a view:
|
||||
|
||||
```bash
|
||||
rea analyze-javascript-application /absolute/path/to/app --json > app-evidence.json
|
||||
jq -c '{source: {kind: "inline", evidence: .}, view: {kind: "summary"}}' app-evidence.json > app-view.json
|
||||
rea inspect-analysis-view app-view.json
|
||||
```
|
||||
|
||||
These commands do not register MCP or install an analysis engine. CLI JavaScript
|
||||
JSON is complete and can be very large; redirect it rather than loading it into
|
||||
context. Use an existing JSON processor if `jq` is absent, without installing
|
||||
one just for this recipe. Inspect a module page or item using the same inline
|
||||
source and the [JavaScript recipes](javascript-applications.md). Preserve graph
|
||||
coverage, limitations and unknowns. Native CLI analysis still requires its
|
||||
selected engine, and all CLI operations retain the host's execution permissions.
|
||||
@@ -1,860 +0,0 @@
|
||||
# Desktop app environments from a request
|
||||
|
||||
Use this when someone asks for an environment of a Linux desktop app with a
|
||||
task, such as "Build me an Inkscape environment where the agent changes a
|
||||
poster's headline and saves it". You are the building agent: you drive the
|
||||
app on a build computer through REA's MCP tools, write the task, export the
|
||||
bundle and witness it in a fresh computer. REA runs no model. The tools are
|
||||
MCP-only and need a Linux host with Docker, agentenv and REA's packed
|
||||
runtime. When a start reports one missing, tell the person to run
|
||||
`rea doctor --provider desktop_build` and apply its fixes, or
|
||||
`rea setup --client <client> --desktop-build` and restart the client, and
|
||||
stop.
|
||||
|
||||
For the whole build:
|
||||
|
||||
- Drive the app only with `act_on_build_computer`. Never script it, run
|
||||
commands in the computer or write its files another way. The one command
|
||||
you write for the computer is a task's reader, which REA runs to check a
|
||||
file; it never makes the seed or the edit.
|
||||
- Ask the upfront questions once, then work on your own. Pause only when
|
||||
blocked.
|
||||
- The seed's content is synthetic. Never put real personal data in it.
|
||||
- Close every computer you start, on success and on failure.
|
||||
- Note the time you start; the report gives the build time.
|
||||
|
||||
## Ask once, upfront
|
||||
|
||||
In one message, ask only what the request leaves open, and keep the list for
|
||||
the report. Skip each question the request answers; when it answers them all,
|
||||
start without asking:
|
||||
|
||||
1. The app and the task, if either is unclear: what changes, in which
|
||||
document.
|
||||
2. Whether they have a starting file of their own; otherwise you make one.
|
||||
3. Whether they want the tasks measured after the witness: with which
|
||||
model, within which budget, and whether to harden a task until it lands
|
||||
in the band.
|
||||
4. If so, whether they want to review the tasks before those solves.
|
||||
|
||||
When you cannot ask, as in a headless `claude -p` run, do not stop: take
|
||||
these defaults for whatever the request leaves open, say which you took in
|
||||
your first message and in the report, and go on. Install from the Ubuntu
|
||||
archive, make a synthetic seed, skip the review, and measure each task
|
||||
within $4 in all, with the model the request names, without hardening.
|
||||
|
||||
## When you are blocked
|
||||
|
||||
Stop, say what you need and wait only for:
|
||||
|
||||
- a licence the app or its installer asks someone to accept: never accept it
|
||||
on your own; name it and where to read it, and go on only once the person
|
||||
says they accept it. Then record their words with `record_build_consent`,
|
||||
kind `licence`, and accept it on their behalf;
|
||||
- credentials: ask the person to write each secret, and nothing else, in a
|
||||
file named after a handle you choose, such as `keepassxc-master`, in
|
||||
`~/.config/rea/build-secrets/` on this host (under `$XDG_CONFIG_HOME` when
|
||||
it is set), readable only by them (`chmod 600`), and wait until they say it
|
||||
is there; then type it with `type_secret` (see Drive the app). Never ask
|
||||
for a password or key in chat, and never read, list or print that folder;
|
||||
- a signed-in state in the seed: by default the export withholds it (see
|
||||
Sign-in and licence state). Seed it only when the person says the bundle
|
||||
may hold it: record their words with `record_build_consent`, kind
|
||||
`seed_sign_in`, and export with `seed_sign_in: true`;
|
||||
- an install this flow cannot do, below.
|
||||
|
||||
Decide everything else yourself and note it in the report.
|
||||
|
||||
## Sign-in and licence state
|
||||
|
||||
What an app writes when someone signs in or activates a licence is withheld
|
||||
from the seed: the bundle's receiver supplies their own. Mark
|
||||
`pre-sign-in` with `mark_build_checkpoint` just before the sign-in or
|
||||
activation and `post-sign-in` just after, both before `seed`: every file
|
||||
under the owned paths that changed between them is withheld, and the draft
|
||||
leaves it out. A file the app writes at first launch, such as a trial's
|
||||
licence file, changes before you can mark anything: name it in
|
||||
`export_build_environment`'s `withhold`. The export lists each withheld file
|
||||
in `withheld_files` with the environment variable a deploy reads it from and
|
||||
the handle of the secret REA keeps its bytes in; a check may not name one.
|
||||
The export refuses a seed file holding a withheld file's exact bytes, such as
|
||||
a `.bak` copy, but a backup that differs from it is not caught: trash backups
|
||||
before `seed`.
|
||||
Explicitly withhold any other files the person identifies as sensitive;
|
||||
REA does not infer secrecy from a value's shape or a file name.
|
||||
|
||||
## Choose the install
|
||||
|
||||
The computer is Ubuntu 24.04 (noble) on this host's architecture only. Use
|
||||
the first source that has the app:
|
||||
|
||||
| Source | `install` |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| Ubuntu archive | `{"kind": "archive", "package": "inkscape"}` |
|
||||
| PPA or vendor apt repository | `{"kind": "apt_repository", "package", "uri", "suite", "components", "key_file"}` |
|
||||
| `.deb` by URL | `{"kind": "package_file", "url", "sha256", "architecture", "dependencies"}` |
|
||||
| AppImage by URL | `{"kind": "appimage", "url", "sha256", "architecture", "extra_packages"}` |
|
||||
| Tarball by URL | `{"kind": "tarball", "url", "sha256", "architecture", "program", "name", "extra_packages"}` |
|
||||
|
||||
- A PPA's `uri` is `https://ppa.launchpadcontent.net/OWNER/NAME/ubuntu`,
|
||||
`suite` `noble`, `components` `["main"]`. `key_file` is the repository's
|
||||
signing key saved on this host; for a PPA, fetch it from
|
||||
keyserver.ubuntu.com by the fingerprint its Launchpad page shows.
|
||||
- When the app exits at launch for want of a runtime piece its package does
|
||||
not install, start a new build computer with that package in
|
||||
`extra_packages`, up to 16 archive names; the export installs them too.
|
||||
MyPaint needs
|
||||
`{"kind": "archive", "package": "mypaint", "extra_packages": ["librsvg2-common"]}`
|
||||
for its SVG icons, and Freeplane `["openjdk-17-jre"]`, since its launcher
|
||||
refuses noble's Java 21. Name one up front, too, when a package the app
|
||||
only recommends holds its content, such as QElectroTech's element library
|
||||
`qelectrotech-data`, without which no component can be placed.
|
||||
- Take the `.deb` path only when the app is not in the archive at all, not
|
||||
to add a package. Ask the requester for its URL and SHA-256; do not fetch
|
||||
a vendor's download page yourself. List its `Depends` as `dependencies`,
|
||||
with any runtime piece it lacks: it installs with the network off.
|
||||
- Take an AppImage or a tarball only when no package has the app. Pin it by
|
||||
the release file's URL for this host's architecture and the SHA-256 its
|
||||
project publishes, such as a GitHub release asset's digest or the
|
||||
release's `SHA256SUMS`; ask the requester when the project publishes none.
|
||||
The computer downloads it when the bundle is put, refuses other bytes, and
|
||||
unpacks it under `/opt/rea-apps/<desktop_entry>` with the network off; the
|
||||
export downloads the same file.
|
||||
- An AppImage installs its own desktop entry, so `desktop_entry` is the
|
||||
`.desktop` file at the top of the image, such as FreeCAD's
|
||||
`org.freecad.FreeCAD`. A wrong name fails the start with the entries the
|
||||
image has: start again with one of them. The image brings its libraries,
|
||||
so `extra_packages` is rarely needed; FreeCAD 1.1.4's needs none.
|
||||
- A tarball has no entry of its own. `program` is the program's path inside
|
||||
the archive, such as `firefox/firefox`; REA writes an entry named
|
||||
`desktop_entry` that runs it with the files it opens, and `name` is what
|
||||
the Applications menu shows. A tarball uses the computer's libraries: name
|
||||
what its program lacks in `extra_packages`. gzip, bzip2 and xz archives
|
||||
are read, zstd ones are not.
|
||||
- Flatpak and snaps are not supported: say so and stop. Flatpak's sandbox
|
||||
needs user namespaces the computer's container does not grant, so a
|
||||
Flathub app installs but never opens; ask for the project's AppImage,
|
||||
tarball or `.deb` when it ships one. An archive package that only installs
|
||||
a snap, such as noble's `firefox`, counts as a snap. A package with no
|
||||
build for this host's architecture (some PPAs are amd64 only) cannot be
|
||||
installed either.
|
||||
- `desktop_entry` is the package's `.desktop` file name under
|
||||
`/usr/share/applications` without `.desktop`, such as
|
||||
`org.inkscape.Inkscape`. Add `window_class` when you know it; Inkscape's
|
||||
is `Inkscape`. AbiWord's entry is `abiword` and its class `Abiword`.
|
||||
- To find the entry, read the package's file list: its page on
|
||||
packages.ubuntu.com, or `apt-get download PKG && dpkg -c PKG_*.deb` where
|
||||
apt runs. The entry may ship in a companion `-data` or `-common` package,
|
||||
as Planner's in `planner-data`: read that package's list too. The entry's `StartupWMClass` may not be the window's real
|
||||
class, so do not take `window_class` from it: MuseScore 3's says
|
||||
`mscore3`, its window's is `MuseScore3`. The export uses the class the
|
||||
capture saw.
|
||||
|
||||
## Drive the app
|
||||
|
||||
- Pass `computer_id` and the action's fields at the top level: click
|
||||
`{x, y, count}`, scroll `{x, y, direction, amount}`, a screenshot region
|
||||
`[x0, y0, x1, y1]`. The rest are in
|
||||
[the build-computer guide](https://github.com/morluto/rea/blob/main/docs/desktop-build-computer.md).
|
||||
- Take a `screenshot` before pointing: points are pixels of the latest
|
||||
frame. If a response is too large, ask for a `region`, or `jpeg` with a
|
||||
lower `quality`. A screenshot with `region` does not capture the screen
|
||||
again; it crops the latest frame, so take a full screenshot, or a `wait`
|
||||
with `screenshot: true`, first. The crop then becomes the latest frame:
|
||||
take a full screenshot before pointing again.
|
||||
- A drawn caret does not prove keyboard focus. After using a toolbar
|
||||
control, click into the document, then type a few characters and look
|
||||
before a long text.
|
||||
- Confirm a dialog with `key` Return, not a trailing `\n` in `type`.
|
||||
- An action's own screenshot is taken at once. After opening a menu, a
|
||||
dialog or a window, look with
|
||||
`{"action": "wait", "ms": 500, "screenshot": true}`.
|
||||
- On a computer started from a request the app is not open at first, and
|
||||
`start_build_capture` opens it: never open it yourself before that. Do not
|
||||
close it while capturing; the capture ends when the app exits. When the
|
||||
app is closed at any other time, open it from Applications → Other → the
|
||||
environment's name.
|
||||
- Type a secret with `{"action": "type_secret", "secret": "<handle>"}`,
|
||||
only into a field that masks what is typed: check in a screenshot that it
|
||||
is a password field, which shows dots. A screenshot shows any field that
|
||||
shows its text, such as many licence-key boxes, so never type a secret into
|
||||
one: say the app needs a secret the build cannot type unseen, and stop.
|
||||
`type_secret` takes no `screenshot`; look with a `wait` after it. A refusal
|
||||
names the secret's file and what is wrong with it: tell the person, never
|
||||
the value.
|
||||
- Prefer shortcuts and typing to long pointer paths: `key` takes `keys`, an
|
||||
array holding one chord's keys, such as
|
||||
`{"action": "key", "keys": ["Control", "s"]}`, and `type` turns `\n` into
|
||||
Enter.
|
||||
- Before `type`, check in a screenshot that a text cursor or a focused
|
||||
field is there. In apps with single-key shortcuts, such as Inkscape, text
|
||||
typed without one runs as commands.
|
||||
- Save As differs by app. Check the dialog's current folder first: most
|
||||
open in the home, so click Documents in the sidebar, but Scribus opens in
|
||||
`~/Documents` and its sidebar has no Documents entry. Type only the file
|
||||
name. A dialog taller than the 1280×800 screen hides its buttons:
|
||||
maximize it from its title bar, or confirm it with `key` Return, as when
|
||||
HomeBank's Save button is off-screen. When a GTK Save dialog ignores
|
||||
clicks or runs off the screen, type `~/Documents/<name>` in its Name field
|
||||
and press Return.
|
||||
- Answer the app's save questions, such as keeping the format, then check
|
||||
the saved path in the title or status bar, or the dialog's folder when
|
||||
the app shows only the name, and that it shows no unsaved mark, before
|
||||
you mark a checkpoint or check. Where the title never shows one, as in
|
||||
HomeBank, a greyed Save or Revert, or the file reopened from recent
|
||||
files, confirms the save.
|
||||
- Qt spin boxes change their value on scroll: move the pointer off them
|
||||
before you scroll.
|
||||
- gLabels edits an object's text in Object properties → Text, not on the
|
||||
canvas.
|
||||
- In Grisbi's transaction form Return moves to the next field; only Return
|
||||
in the last field, Notes, records the transaction.
|
||||
- ghostwriter autosaves a new document as `~/Documents/untitled-1.md`: name
|
||||
it with File → Rename, not Save As; Ctrl+Shift+S does nothing, and Enter
|
||||
after a checklist item starts another `- [ ]` item.
|
||||
- KMyMoney's first launch opens the New File Setup wizard; Finish asks for a
|
||||
storage type (XML), then a Save As where typing `Documents/<name>.kmy`
|
||||
works. Its date field is a sectioned Qt editor: double-click the month and
|
||||
type each part, since Ctrl+A garbles it. A new transaction's payee comes
|
||||
prefilled and selected. Amounts are saved as reduced fractions, such as
|
||||
`-1749/20`, so compare them exactly. Untick Help → Tip of the Day → "Show
|
||||
tips on startup" before `seed`, or the tip appears at every boot.
|
||||
- QElectroTech: an element dropped from Collections follows the pointer until
|
||||
a click on the folio places it. Its title shows only the app, so it needs
|
||||
`state_paths`; its settings are in `~/.qet`.
|
||||
- Rosegarden: after a dialog its Transport window can take keyboard focus,
|
||||
so save with File → Save.
|
||||
- Pencil2D: the Tools dock is too narrow for its buttons, so use shortcuts
|
||||
(B for the brush); double-click a layer's name to rename it.
|
||||
- Umbrello: the title shows no unsaved mark, so confirm a save with a probe
|
||||
checkpoint. An Untitled document autosaves `~/autosave.xmi`: delete it
|
||||
before `seed`. Add attributes from the tree: right-click the class → New →
|
||||
Attribute.
|
||||
- Hydrogen: the computer has no audio device, so Hydrogen falls back to a
|
||||
null driver and its BPM field snaps back to 120 within about 100 ms.
|
||||
Before `seed`, set Preferences → Audio System → ALSA with device `null`
|
||||
and click "Apply and restart output", which `~/.hydrogen/hydrogen.conf`
|
||||
keeps for the seed; also set autosaves per hour to 0, and dismiss the
|
||||
development-version dialog with "Don't show again". `pulseaudio` as an
|
||||
extra package does not help.
|
||||
- Poedit cannot add entries by hand. Use File → New, pick the language and
|
||||
save to `Documents` first (Sources Paths stays locked until then), then
|
||||
Properties → Sources Paths → + → Add Files with a source such as
|
||||
`/usr/lib/python3.12/argparse.py`. It writes `fr.mo` beside the `.po`, and
|
||||
the language box's autocomplete popup takes the first OK click.
|
||||
- CherryTree: its first-run autosave "Choose Storage Type" dialog swallows
|
||||
keys, so save once early as `.ctd` and untick autosave.
|
||||
- VSCodium (its tarball, with `extra_packages: ["libnss3",
|
||||
"libasound2t64"]`) runs on outside REA's reach, so `stop_build_capture`
|
||||
fails to clean it up: after `end`, quit it with Ctrl+Q, then stop. Enter
|
||||
accepts its word suggestions, so press Escape after typing a line.
|
||||
- Glade: a new widget's id is empty until you set it, and a second save
|
||||
writes a `.glade~` backup, which shows in the draft as an unrelated
|
||||
change. A backup present at `seed` is seeded with the document, so trash
|
||||
it before `seed`. The draft names a button's `<child>` by the id
|
||||
its widget carries, `[name=child][>object@id=submit_button]`, which moving
|
||||
the button keeps; give the widget an id before `seed` so it can.
|
||||
- VYM: the title never names the map, so pass `Documents/<map>.vym` in
|
||||
`state_paths`. Its settings live in `.config/InSilmaril` and are written
|
||||
only at a clean exit. It writes `<map>.vym.lock` while a map is open, so
|
||||
close the map before `seed`. Press A only after clicking a branch on the
|
||||
canvas: keys typed in the tree editor run other shortcuts.
|
||||
- KmPlot: Create → Cartesian Plot, then edit the formula in the Function
|
||||
tab and press Return. It stores minus as U+2212 (`−`). Its file:// title
|
||||
still names the file.
|
||||
- RedNotebook keeps each month in `~/.rednotebook/data/YYYY-MM.txt`, YAML
|
||||
read as text, folded at about column 80: pick check phrases the fold keeps
|
||||
on one line.
|
||||
- Qt5 apps saving with QDom, such as KmPlot, write attributes in a random
|
||||
order in each process, so check their XML by element, never by whole line.
|
||||
- Gnumeric's desktop entry is `org.gnumeric.gnumeric`. Its Save As dialog
|
||||
runs off the 1280×800 screen: type `~/Documents/<name>` in Name and press
|
||||
Return. Rename a sheet with right-click on its tab, then Rename….
|
||||
- Parley's title shows the collection's title, so name the file in
|
||||
`state_paths`. File → New opens the new collection's Properties dialog:
|
||||
confirm it with OK, since Cancel keeps the old collection and its autosave
|
||||
lock open. A new collection starts with 15 blank entries: delete them.
|
||||
Pick languages from the dropdown; a typed name keeps the previous locale.
|
||||
While a document is open KDE keeps an autosave and its `.lock` in
|
||||
`~/.local/share/stalefiles/parley/`, and there is no File → Close: before
|
||||
`seed`, use File → New, then reopen the collection from Open Recent.
|
||||
- Sigil: pass `extra_packages: ["libqt5svg5"]`, or its SVG toolbar icons are
|
||||
blank. Its title names the book, so it needs no `state_paths`. It unpacks
|
||||
the open book into `.local/share/sigil-ebook/sigil/workspace/Sigil-XXXXXX/`
|
||||
and deletes it on exit, so never name the whole `sigil-ebook` folder: that
|
||||
seeds the stale copy.
|
||||
- What the app saved under the home before `seed`, such as a document in
|
||||
`~/Documents` or its settings, is part of the seed. A file you move to the
|
||||
Trash is left out, and the export lists it in `limitations`. Trash a stray
|
||||
file rather than starting over. JabRef 3.8 writes `<file>.bak` and
|
||||
`<file>.sav` beside a `.bib` it saves; in the owned folder they are seeded
|
||||
and show in the draft as noise, so trash them before `seed` if they should
|
||||
not ship.
|
||||
- Dismiss first-run dialogs, such as a welcome screen, before `seed`. A
|
||||
choice the app saves only later, such as Inkscape's "show this every
|
||||
time", may not reach the seed, so expect the dialog in the witness too.
|
||||
- Some apps write their settings late: Freeplane writes
|
||||
`~/.config/freeplane/1.7.x/auto.properties` only once Preferences is
|
||||
confirmed, and a seed without it reopens its documentation map. Before
|
||||
`seed`, open the app's preferences and confirm them, then read a probe
|
||||
checkpoint's `changed_files` to see the settings file written.
|
||||
- Before `seed`, click into the document to clear any selection the app
|
||||
would save: Gnote saves its cursor and selection with the note.
|
||||
- An app whose state folder is named after it, such as
|
||||
`~/.local/share/gnote`, or after it and its version, such as TreeLine's
|
||||
`~/.treeline-3.1`, is found without `state_paths`.
|
||||
- TreeLine: File → New opens a template window. Book List's CATEGORY makes
|
||||
AUTHOR children by default; change it under Data → Configure Data Types →
|
||||
Default Child Type. A new child opens an inline rename showing "", so
|
||||
press Escape and fill its fields in the Data Edit tab. Ctrl+A adds a child
|
||||
node even while a field has focus. Clicking a node already selected starts
|
||||
an inline rename, so Ctrl+X then cuts its text: cut a node from its
|
||||
context menu. It saves nodes sorted by their
|
||||
generated uid1, so their order changes from run to run. The draft names a
|
||||
new node by its format and a field of its `data`,
|
||||
`/nodes/[format=BOOK][>data~1Title=The Silk Roads]`, and checks its other
|
||||
fields through that, so give each new node a title no other node has.
|
||||
- After an app crashes, capture again on a fresh computer, so the seed
|
||||
carries no crash-recovery state, such as MyPaint's "Recover interrupted
|
||||
work?".
|
||||
- Give what the task changes a stable name where the app has one, such as an
|
||||
id in Inkscape's Object Properties, so a check can select it.
|
||||
- Expect apps to split text into runs: Writer's revision ids split an
|
||||
edited paragraph into spans and may give it an automatic style such as
|
||||
`P1`, and AbiWord wraps each insertion in `<c>`. Check such text whole
|
||||
with `text_equals` (see Write the task), and find the paragraph by
|
||||
something the edit keeps, such as AbiWord's `xid`.
|
||||
- Freeplane's first launch opens a documentation map: close it with Ctrl+W.
|
||||
Its Ctrl+N asks for a template.
|
||||
- MyPaint 2.0.1 crashes on brush strokes on the build computer: build its
|
||||
seed with flood fill and layer operations.
|
||||
- GnuCash's Ctrl+S on a new book opens a Save dialog with no format choice:
|
||||
use File > Save As to pick XML. Close the book (File > New File) before
|
||||
`seed`, so its `.LCK` lock is not seeded.
|
||||
|
||||
## Build the seed and the edit
|
||||
|
||||
1. `start_build_computer` with
|
||||
`{"source": {"kind": "request", "desktop_entry": ..., "install": ...}}`.
|
||||
The first start of an app builds its image, and starts at the same time
|
||||
on this host wait for each other; either can take minutes. A failed
|
||||
start returns the build's error: fix the request, or stop when the
|
||||
install cannot work, and close any `computer_id` the error names.
|
||||
2. `start_build_capture`. It returns once the app's window is open. A
|
||||
`package_lists_error` may stop the capture finding the package's
|
||||
sources; the export says when that matters. When the app exits before
|
||||
its window opens, the error gives its exit code and the end of its
|
||||
stderr, which may name what it lacks, such as an icon loader or a Java
|
||||
runtime: close the computer and start a new one with that package in
|
||||
`extra_packages` (see Choose the install), or stop as for an install
|
||||
this flow cannot do.
|
||||
- REA counts a file as the app's document only when the app's window
|
||||
title shows its file name or `~/` path. When the title shows something
|
||||
else, such as MuseScore's score title, pass the path you will save the
|
||||
document at in `state_paths`, relative to the home:
|
||||
`{"computer_id": ..., "state_paths": ["Documents/MuseScore3/Scores/Morning_Song.mscz"]}`.
|
||||
It need not exist yet. Add the app's settings folder when its name
|
||||
differs from the app's, such as `.config/MuseScore`, so the seed keeps
|
||||
the dismissed first-run dialogs. Do not pass the document as a launch
|
||||
argument to make REA see it: `state_paths` is the way. gLabels' title
|
||||
leaves out the file's extension and Grisbi's shows the entity's name,
|
||||
so both need it.
|
||||
- An app whose document is a folder of pages, such as a Zim notebook
|
||||
(`Notebooks/Notes`, a `.txt` file per page), never shows a file in its
|
||||
title: pass the folder in `state_paths`. Every page in it is then a
|
||||
document, so the bundle opens none at launch and the app starts where
|
||||
it keeps its place; check the page the task changes.
|
||||
3. Make the seed through the app and save it from the app under
|
||||
`~/Documents`: build the document in the app, or write its source where
|
||||
the app edits its format as text, then save it again from the app. Make
|
||||
it look real but synthetic: the thing the task changes, plus a few things
|
||||
the change must leave alone. Keep it to one document: the bundle's app
|
||||
opens the seed's document at launch only when there is exactly one, and
|
||||
otherwise starts on its own screen. For a starting file of the requester's,
|
||||
read it here and rebuild it in the app with synthetic data in place of
|
||||
personal data; REA cannot copy a file into the computer.
|
||||
- An app that keeps its own data and saves after every change, such as
|
||||
Osmo's contacts in `~/.local/share/osmo/contacts_records.xml`, has no
|
||||
document: that state is the seed. Build it in the app and mark `seed`
|
||||
once the app has written. To see whether it has, mark a throwaway
|
||||
checkpoint, such as `probe-1`, and read its `changed_files`: up to ten
|
||||
files that changed since the checkpoint before, those a window title
|
||||
named first, then those under `state_paths` and those the app held
|
||||
open, caches last, with `more_changed_files` counting the rest.
|
||||
- The app's own compressed format works when it is gzip, such as
|
||||
Gnumeric's `.gnumeric`, Dia's `.dia` or Inkscape's `.svgz`: REA reads
|
||||
the document inside, and checks point into it as into the same
|
||||
document uncompressed. Compression may be off by default: Dia's
|
||||
"Compress diagram files" is unticked, which REA reads as plain XML. A format REA reads as binary, such as one
|
||||
compressed with xz, has no view: save in a format the app offers that
|
||||
REA reads, then reopen the file once and check that a plain Ctrl+S
|
||||
keeps that format. When the task is about the app's own binary
|
||||
format, such as GIMP's `.xcf` or Blender's `.blend`, keep it and check
|
||||
it through a reader (see Read a binary file with a reader).
|
||||
4. Only once the seed is saved, `mark_build_checkpoint` with name `seed`.
|
||||
5. Make the change the task asks for, as a user would, and save it.
|
||||
6. Only once the change is saved, `mark_build_checkpoint` with name `end`.
|
||||
For more tasks in this environment, make each next task's change on top
|
||||
of the last as a user would, save it, and mark `end-<task name>`, such as
|
||||
`end-add-bread`. Each task is checked at its own end, so its checks must
|
||||
not read what a later task changes, and a solving agent starts each from
|
||||
the seed, so a task must not need an earlier one done first.
|
||||
Then, for each task you can, make a near miss of its change on top, a
|
||||
plausible wrong edit such as a typo or a wrong number, save it and mark
|
||||
`near-<task name>`: the export refuses a task whose near miss passes every
|
||||
check. Take the near misses after the last end, since each later draft
|
||||
starts from the end before it.
|
||||
7. `stop_build_capture`. Read `draft.files` (each file that changed, with
|
||||
its net change and its `values`), `draft.task.checks` (suggested
|
||||
checks), `draft.guards`, `draft.notes` and `limitations`; each
|
||||
`later_drafts` entry is the same for one later task, from the end
|
||||
checkpoint before its own, so it shows only that task's change. When
|
||||
`draft` is null, read `draft_refusal` first: a missing `seed` or `end`
|
||||
checkpoint means capturing again on a new build computer, while a failed
|
||||
draft command names its own cause to fix. If a declared secret was used,
|
||||
automatic drafting is withheld: write the task manually and export it.
|
||||
When `draft.files` is empty,
|
||||
`draft.notes` says why:
|
||||
- `no_change`: nothing was saved between `seed` and `end`. Capture again
|
||||
on a new build computer, and save before marking `end`. When the app
|
||||
keeps the task's state only on screen, such as a calculator's result,
|
||||
judge the screen instead (see Judge the screen).
|
||||
- `unowned_changes` with `no_document`: the app saved your document
|
||||
where REA did not count it as the app's. Its `files` list what changed,
|
||||
with their roles; the document is among them. Close the computer and
|
||||
capture again on a new one with that path in `state_paths`; the export
|
||||
refuses a check on a file outside the owned paths.
|
||||
- `uncheckable_changes`: the app saved the change in dconf, as gedit does
|
||||
its preferences, and no check reads dconf. Saving again does not help:
|
||||
choose a change the app saves in a file, and capture again on a new
|
||||
build computer, or judge the screen (see Judge the screen).
|
||||
- `unreadable_changes`: a dconf database changed but REA could not read
|
||||
it; `error` says why. This is not evidence the app saved there: report
|
||||
the error rather than changing the task.
|
||||
|
||||
A note with a `file` says why that file's suggestions are thin:
|
||||
- `by_position`: only position names what changed; its `elements` lack
|
||||
a naming attribute. Name a record that keeps its fields as child
|
||||
elements by one of them, as Osmo's contact by
|
||||
`[name=contact][>first_name=Delphine]`, a JSON record by a field
|
||||
inside it, `[>data/Title=…]` (`~1` for the `/` inside a pointer), or
|
||||
an element by an id its child carries, `[>object@id=…]`; count records
|
||||
with `member_count`, `matching` `[name=contact]`; otherwise check an
|
||||
ancestor's text, or give the element an id in the app and capture again.
|
||||
- `view_state_only`: every change is view state, a renumbered id or a
|
||||
time the app stamps, and its `pointers` are the checks kept. The edit
|
||||
was likely not saved: capture again and save it before `end`.
|
||||
- `long_text`: the element's text is too long to check whole; check a
|
||||
phrase in it with `text_contains`.
|
||||
- `cut_view` or `unreadable_view`: REA's view holds only part of the
|
||||
file, or none a check can read, such as a binary format (step 3). For
|
||||
a binary file, read it with a reader (see Read a binary file with a
|
||||
reader).
|
||||
|
||||
## Write the task
|
||||
|
||||
Write `{"name", "prompt", "checks"}` from the draft and what you did, one
|
||||
per task, each with its own name.
|
||||
|
||||
- `name`: lowercase letters, digits and hyphens, not `smoke`.
|
||||
- `prompt`: what the requester wants, in their words and the app's terms,
|
||||
starting with the app's name, naming the document as a user would and
|
||||
ending with saving it: "In Inkscape, change the poster's headline in
|
||||
Documents/poster.svg to Summer Bake Sale and save it." Name the app: the
|
||||
solving agent's own shell cannot see the computer's files, and the app's
|
||||
name sends it to the screen. Leave saving out for an app that saves on its own, after every change
|
||||
or a few seconds after the last one, such as Osmo or Gnote: grading waits
|
||||
until the app's files have held for 3 s, so the solving agent need not
|
||||
wait for the save. A save more than 3 s after the agent stops is still
|
||||
missed; name such an app in the report. Never say how the file stores it,
|
||||
and never give the checks.
|
||||
- Checks are file-based: content checks that prove the edit, never only that
|
||||
a file exists or changed. Start from the suggestions; keep those that say
|
||||
what the user did and drop what follows from how the app saves.
|
||||
- Compare exact values, so a typo or a wrong number fails.
|
||||
- Read each file's `values` before writing checks: every value the change
|
||||
added, changed and removed, with `before` and `after` (an uncut scalar, or
|
||||
a preview cut at 600 characters) at the pointer a check names it by. Copy
|
||||
the pointer. Copy `after` into `value` only when it is an uncut scalar: a
|
||||
string without a trailing `…`, a number, or a boolean. Compact XML and a
|
||||
JSON array or object are preview strings, so a `value_equals` check needs
|
||||
the structured value from the file's view, and element text uses
|
||||
`text_equals` or `text_contains`.
|
||||
Only XML files, archives, JSON and key files have values: a text file's
|
||||
or SQLite database's `change` already lists its lines and rows.
|
||||
`refers_to` says what a number or id stands for, such as `category="44"`
|
||||
naming `<cat key="44" name="Groceries"/>`, or a new KMyMoney
|
||||
transaction's splits naming their account and payee; use it for the
|
||||
prompt's words. `alternatives` gives a changed value's pointer by the
|
||||
element's other names, such as a label beside an id, to pick another key
|
||||
without another export.
|
||||
Skip entries whose `note` is `by_position` (no check can use that
|
||||
pointer) or noise (`view_state`, `renumbered`, `volatile`,
|
||||
`timestamp`, `upkeep`, `layout`). `values_cut` means a limit left some out.
|
||||
- Select elements by what they hold, never by position: SVG text by id,
|
||||
`/root/children/[name=g][@inkscape:label=Layer 1]/children/[name=text][@id=headline]/children/[name=tspan]/children/0`,
|
||||
or an ODF paragraph by style,
|
||||
`/entries/[name=content.xml]/view/root/children/[name=office:body]/children/[name=office:text]/children/[name=text:p][@text:style-name=Title]/children/0`.
|
||||
A selector must match exactly one element, so give the target a unique id
|
||||
or style in the seed. The draft's pointers already do this: a name-like
|
||||
attribute such as Scribus's `ANNAME` before a coordinate, and two
|
||||
attributes together where one is not enough, as a spreadsheet cell's
|
||||
`[@Col=3][@Row=2]`. Only an element's text is read by position, as
|
||||
`children/0`.
|
||||
- When an app may split an element's text into runs, check it whole:
|
||||
`text_equals` (or `text_contains` for a value inside a sentence) on the
|
||||
element compares all its text, however it is split. Add
|
||||
`normalize_whitespace: true` when the app may reflow spaces. Example, an
|
||||
AbiWord paragraph by its stable id:
|
||||
`{"kind": "text_contains", "file": "Documents/memo.abw", "pointer": "/root/children/[name=section]/children/[name=p][@xid=6]", "value": "at 3:30 PM"}`.
|
||||
Point a text check at the paragraph or another element the edit keeps,
|
||||
never at a run such as AbiWord's `[name=c]`: a reopened document splits
|
||||
the text into several runs. The draft lists only what changed; an
|
||||
unchanged element is in `draft.guards` when it is a like sibling of a
|
||||
changed one, and otherwise use the app's usual numbering, such as another
|
||||
paragraph's `xid`, and let the export confirm it.
|
||||
- In an ODS spreadsheet no attribute names a row: the draft names it by its
|
||||
label cell, `[name=table:table-row][>table:table-cell=February]`. Where no
|
||||
cell names it alone, name the sheet in the seed, and check its whole
|
||||
text: `text_equals` on
|
||||
`/entries/[name=content.xml]/view/root/children/[name=office:body]/children/[name=office:spreadsheet]/children/[name=table:table][@table:name=Q1 Sales]`,
|
||||
or a stretch of it with `text_contains`. Cell texts join with nothing
|
||||
between, so a row reads `February23100`.
|
||||
- For a line added to free text, such as an item on a note's list, the
|
||||
draft compares the whole text with `text_equals`. This rejects longer or
|
||||
negated items, but can reject a correct edit with different ordering or
|
||||
spacing. Report that limitation when the task permits such variations.
|
||||
- For a note placed by its own `x` and `y`, such as Xournal++'s `<text>`
|
||||
on a layer, the draft checks its text on the layer with `text_contains`,
|
||||
which passes wherever the user puts it, and guards each note it kept.
|
||||
- Some apps keep text in an attribute, one element per run, such as
|
||||
Scribus's `<ITEXT CH=...>`: add `"text_attribute": "CH"` to `text_equals`
|
||||
or `text_contains` on the frame,
|
||||
`/root/children/[name=DOCUMENT]/children/[name=PAGEOBJECT][@ANNAME=Headline]`,
|
||||
to read its runs' values joined.
|
||||
- When the draft shows no values or no checks, a scratch export can expose
|
||||
`envs/<env>/seed/` only after the task has at least one valid check: it must
|
||||
pass on the captured end state and fail on the seed. Before the first
|
||||
successful export, use `act_on_build_computer` to open the saved file in an
|
||||
available GUI text editor or viewer and inspect its actual representation
|
||||
without changing it. Use the visible saved content and the task's edit to
|
||||
write a check; an export refusal can also name the end state's value, such
|
||||
as `is "44", not "1"`. If the file cannot be inspected or expressed by a
|
||||
supported check, report the limitation instead of guessing a check.
|
||||
- Where easy, add one check that the rest is untouched, such as another
|
||||
text's value the edit must leave alone. `draft.guards` offers them at
|
||||
their seed values, which the end holds too: a field on the nearest like
|
||||
siblings, one the user sets, such as Planner's `work`, before a default
|
||||
they all share; the text an addition kept; a text file's lines beside its
|
||||
new one. Copy one or two into `checks`; they pass on the seed, which is
|
||||
fine, since the export needs only one check to fail there.
|
||||
- The draft suggests a decimal the app writes as text, such as `146.83`,
|
||||
as a number with `tolerance: 0.001`; keep the tolerance for any number
|
||||
you check that way.
|
||||
- When the draft shows an XML document's changes as `lines`, REA read it
|
||||
as text, for example because its DOCTYPE declares entities. Selectors are
|
||||
impossible there, and `line_count` with `contains` only approximates a
|
||||
value: `3:30` also matches `13:30`. Bound it with neighbouring text and
|
||||
name the check as approximate in the report's limitations.
|
||||
- Some apps save a document reloaded from disk differently from one typed
|
||||
in the same session: AbiWord wraps each new insertion in its own markup.
|
||||
Your build's end state hides this, which is why whole-text checks matter;
|
||||
expect the witness to show the real format.
|
||||
|
||||
The kinds and pointer rules are in
|
||||
[the desktop tasks section](environments.md#pick-the-checks).
|
||||
|
||||
### Read a binary file with a reader
|
||||
|
||||
When the draft notes `unreadable_view` on a binary document, add a reader to
|
||||
the task: the app's own headless command, which REA runs in the computer on a
|
||||
private copy of the file, and whose output a check reads instead of the file.
|
||||
Write it in the app's scripting mode, print each fact on a line starting
|
||||
with `REA ` (with the space), and name the copy `{file}`:
|
||||
|
||||
- GIMP `.xcf`, layer names, as `lines`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "layers",
|
||||
"file": "Documents/poster.xcf",
|
||||
"argv": ["gimp-console-2.10", "-i", "-d", "-f", "-b", "-"],
|
||||
"stdin": "(let ((image (car (gimp-file-load RUN-NONINTERACTIVE \"{file}\" \"{file}\")))) (for-each (lambda (layer) (display \"\\nREA layer \") (display (car (gimp-item-get-name layer)))) (vector->list (cadr (gimp-image-get-layers image)))) (newline))\n(gimp-quit 0)\n",
|
||||
"output": "lines"
|
||||
}
|
||||
```
|
||||
|
||||
checked with `{"kind": "lines_present", "file": "Documents/poster.xcf", "reader": "layers", "lines": ["layer Summer Sale"]}`.
|
||||
Script-Fu's prompt `ts> ` starts a line, so print a newline before each
|
||||
marked line, as `\nREA` does. A text layer's text is
|
||||
`(car (gimp-text-layer-get-text layer))`.
|
||||
|
||||
- Blender `.blend`, objects with their type and location, as `json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "objects",
|
||||
"file": "Documents/scene.blend",
|
||||
"argv": [
|
||||
"blender",
|
||||
"-b",
|
||||
"--factory-startup",
|
||||
"{file}",
|
||||
"--python-expr",
|
||||
"import bpy, json; print('REA ' + json.dumps({o.name: {'type': o.type, 'location': [round(v, 4) for v in o.location]} for o in bpy.data.objects}, sort_keys=True))"
|
||||
],
|
||||
"output": "json"
|
||||
}
|
||||
```
|
||||
|
||||
checked with `{"kind": "value_equals", "file": "Documents/scene.blend", "reader": "objects", "pointer": "/Crate/location/0", "value": 2, "tolerance": 0.001}`.
|
||||
Print every value a check compares as a number or string a check can
|
||||
name, rounded where the app stores floats.
|
||||
Blender 4 shows its Quick Setup splash at every launch until its
|
||||
preferences are saved: with `.config/blender` in `state_paths`, confirm
|
||||
Quick Setup with Continue, save the preferences (Edit → Preferences, then
|
||||
Save Preferences in the menu at its bottom left), and read a probe
|
||||
checkpoint's `changed_files` for `.config/blender/4.0/config/userpref.blend`
|
||||
before `seed`, so solves start on the scene. Blender often takes the first click on a
|
||||
menu or a dialog's button only to focus it: click again.
|
||||
|
||||
- A format LibreOffice opens but REA cannot read, such as a legacy `.xls`,
|
||||
converted with `--convert-to`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "cells",
|
||||
"file": "Documents/budget.xls",
|
||||
"argv": [
|
||||
"sh",
|
||||
"-c",
|
||||
"soffice --headless --convert-to csv --outdir out \"$1\" >/dev/null && sed 's/^/REA /' out/*.csv",
|
||||
"sh",
|
||||
"{file}"
|
||||
],
|
||||
"output": "lines",
|
||||
"timeout_s": 120
|
||||
}
|
||||
```
|
||||
|
||||
Pass `{file}` to `sh -c` as an argument, as here, rather than inside the
|
||||
script.
|
||||
|
||||
The reader runs without the display, in a fresh home with the app's default
|
||||
settings, so use the app's headless or batch mode; set `display: true` only
|
||||
for an app that has none. It must exit 0 within `timeout_s` (60 s by
|
||||
default, at most 300) and print at most 1 MiB. Pick facts the change sets and
|
||||
the near miss gets wrong, such as a name or a number, not the app's whole
|
||||
output.
|
||||
|
||||
Give the task the reader in `readers`, and each check through it `reader`
|
||||
and the reader's `file`. `json` readers serve `value_equals`, `value_absent`
|
||||
and `member_count`; `lines` readers `lines_present`, `lines_absent` and
|
||||
`line_count`. The export runs the reader in the build computer on the seed
|
||||
and the end: when a check through it fails on the end, the refusal shows
|
||||
what the reader printed, and a reader that fails shows its error and the end
|
||||
of its stderr. Fix the reader or the check from that and export again.
|
||||
|
||||
### Judge the screen
|
||||
|
||||
Only when no file shows the change, as when the draft notes `no_change`
|
||||
because the app keeps its state on screen, write the task's checks as
|
||||
screen judges: `{"id": "shown", "kind": "screen_judge", "question": "Does the
|
||||
calculator's display show 8.568?"}`. A model, the judge, answers the question
|
||||
on a screenshot three times, and the check passes when most answers are yes.
|
||||
|
||||
- REA keeps a screenshot at each checkpoint. Mark `seed` with the screen
|
||||
showing the state before the task, where the answer is no, and `end` with
|
||||
the change showing, where it is yes: the bundle ships both as the judge's
|
||||
examples.
|
||||
- Ask about what the change makes visible, with exact values, so the near
|
||||
miss is a no: a result, a label, a selected item. Avoid what a window
|
||||
moved or a cursor blinking can change.
|
||||
- The prompt asks to leave the result showing, since the judge sees only
|
||||
the screen when the agent stops.
|
||||
- A task with screen judges holds only screen judges and no readers. The
|
||||
export refuses one when a file shows the change, and checks none of its
|
||||
questions: the witness does, running the bundle's own judge with the
|
||||
runner's model endpoint (`LITELLM_BASE_URL` and `LITELLM_API_KEY`, or
|
||||
`[model]` in `.agentenv/config.toml`) on a fresh screenshot at each stage.
|
||||
Without an endpoint the witness's check fails: say what to set and stop.
|
||||
- The judge's model is `[model.roles] judge`, else `[model] default`, else
|
||||
`anthropic/claude-sonnet-5-5`. `agent-env run --judge-model` does not
|
||||
reach it. Each grade costs about $0.02.
|
||||
|
||||
## Export
|
||||
|
||||
`export_build_environment` with `tasks` and `directory`, the absolute path of
|
||||
a new or empty folder here, such as `<home>/rea-builds/poster/bundle`. Each
|
||||
entry of `tasks` is `{"task": ..., "end_checkpoint": ...}`: leave
|
||||
`end_checkpoint` out for the task checked at `end`, and give
|
||||
`end-<task name>` for each later one. The
|
||||
export seeds what was saved at `seed`, replaces the computer's host name,
|
||||
`user@host` and home path (a bare user name, such as AbiWord's author
|
||||
`user`, stays), and checks each task against `seed`, its own end and its
|
||||
`near-<task name>` when you took one. It refuses a task
|
||||
unless at least one check fails on the seed, every check passes at its
|
||||
end and one fails at its near miss, and says which task and why, such as a
|
||||
pointer that names nothing at the end. A
|
||||
check that the rest is untouched passes on the seed too, which is fine. On
|
||||
a refusal, fix the task and export again, into another folder if that one
|
||||
is no longer empty. The bundle's eval lists every task, so a run of the
|
||||
bundle runs them all. A refusal naming a secret's handle means a file of the
|
||||
bundle holds that secret, most often an app's sign-in or licence state in
|
||||
the seed: do not work around it, but report it and stop. Keep
|
||||
`seed_files`, `scrubbed_files`, `unscrubbed_files` and `limitations` for the
|
||||
report.
|
||||
|
||||
The limitations name the folders the app wrote that the bundle neither owns
|
||||
nor seeds, such as `~/.config/MuseScore`, so the app starts without them:
|
||||
expect its first-run wizard, a tour or a "restore session?" question at every
|
||||
boot. If the witness shows one, capture again on a new build computer with
|
||||
those folders in `state_paths`, and dismiss the dialogs before `seed`. Lock
|
||||
files the app holds while it runs, such as LibreOffice's `.lock` and
|
||||
`.~lock.<name>#`, GnuCash's `<book>.LCK` or Grisbi's `.<file>.lock`, are
|
||||
left out of the seed and listed after "Left out of the seed". An app can
|
||||
still start fine yet refuse every save when a lock was seeded, so close the
|
||||
document before `seed` where the app lets you (apps other than LibreOffice,
|
||||
such as Rosegarden, also write `.~lock.<name>#` and autosaves beside a
|
||||
document, and closing it covers them too), and treat "file is locked"
|
||||
on a save in the witness as a seeded lock: delete it from the bundle's seed
|
||||
and witness again in a new computer. In LibreOffice, own the profile
|
||||
(`state_paths: [".config/libreoffice"]`) for a clean boot, and close the
|
||||
document before `seed`, then reopen it from Recent Documents to make the
|
||||
change: the export also leaves out the recovery list LibreOffice writes while
|
||||
a document is open, which would start the bundle into document recovery. A
|
||||
limitation naming `~/.config/dconf/user` as unread means the app's dconf
|
||||
settings, such as a first-run tip you turned off, are not seeded: capture
|
||||
again on a new build computer. The limitations also name seeded WebKit,
|
||||
Chromium or QtWebEngine storage and caches; when the witness's boot is clean,
|
||||
list them in the report.
|
||||
|
||||
Keep the build computer open until the witness holds: a task fix exports
|
||||
again from it.
|
||||
|
||||
## Witness
|
||||
|
||||
Witness each task in its own fresh computer of the bundle, from the prompt,
|
||||
as a solving agent would; a computer is witnessed once, so the steps below
|
||||
repeat for each task, each with its own record folder. Do not replay the build's actions or coordinates, and do not
|
||||
check between stages to steer the edit: a solving agent never sees the
|
||||
checks. Give every `check_build_task` the task exactly as exported.
|
||||
|
||||
1. `start_build_computer` with
|
||||
`{"source": {"kind": "bundle", "path": "<bundle>"}}`, adding
|
||||
`"settings": {"<variable>": "<handle>"}` for each of the export's
|
||||
`withheld_files`, or the deploy is refused for want of them, and agentenv
|
||||
keeps that bundle's build failed, so start from a fresh copy of it. The app starts
|
||||
with the seed's files in place and opens the seed's document when the
|
||||
seed holds exactly one and the app takes a file. When the export's
|
||||
`limitations` say it opens no document, open it from the prompt's path,
|
||||
as a solving agent would. Take a `wait` screenshot first, since the
|
||||
desktop may not have drawn yet. When the start fails because the
|
||||
computer never got ready, and its error names lock files in the bundle's
|
||||
seed, delete them from the bundle and start again.
|
||||
2. `check_build_task` with `stage: "seed"` and `directory`, the absolute
|
||||
path of a new or empty folder for the record. A check must fail.
|
||||
Recording starts. Later stages take no `directory`.
|
||||
3. Do the task from the prompt with `act_on_build_computer` and save.
|
||||
`check_build_task` with `stage: "after_edit"`: every check must pass.
|
||||
Each check waits until the app's files have held for 3 s, as the
|
||||
bundle's verifier does, so an app that saves on its own needs no wait of
|
||||
yours. A result with `settled: false` means the files were still
|
||||
changing at the 15 s bound, and `read_cut: true` that a slow read was
|
||||
still running there.
|
||||
4. Make a near miss on top of it: a plausible wrong edit, such as a typo or
|
||||
a wrong number, and save. `check_build_task` with `stage: "near_miss"`:
|
||||
a check must fail.
|
||||
5. `act_on_build_computer` with `action: "reset"`, then `check_build_task`
|
||||
with `stage: "after_reset"`: a check must fail again.
|
||||
6. The last stage ends the witness: `witness.record` has the verdict, the
|
||||
path of `witness.json` and the recording's segments in `directory`.
|
||||
`close_build_computer` on the witness computer.
|
||||
|
||||
A stage that does not hold ends the witness as `failed`. Read each check's
|
||||
result and decide:
|
||||
|
||||
- `after_edit` failed though the saved file is right: the check is brittle.
|
||||
Fix it, export again and witness in a new computer.
|
||||
- `after_edit` failed because your edit was wrong: witness again in a new
|
||||
computer; a computer is witnessed once.
|
||||
- `near_miss` passed: the checks are too loose. Tighten them, export again
|
||||
and witness again.
|
||||
- `after_reset` passed: the reset did not restore the seed. Report it as a
|
||||
limitation.
|
||||
|
||||
Stop after three witnesses of a task and report what still fails.
|
||||
|
||||
## Measure
|
||||
|
||||
Only if the requester asked for it, or by the headless default above. If
|
||||
they wanted to review the tasks first, show them each prompt and checks and
|
||||
wait for their approval. Then `measure_build_task` each task with `bundle`,
|
||||
`task`, `model` and `budget_usd`, its share of the budget. It runs the task
|
||||
in fresh computers, 3 times, then 2 more unless they passed none or all, and
|
||||
keeps a task that passes 1 to 3 of 5 (`band: "in_band"`). It stops before a
|
||||
solve the largest cost so far would take past the budget, and writes the
|
||||
record into the bundle's `difficulty.json` and README. The solver needs a
|
||||
model endpoint where REA starts: `LITELLM_BASE_URL` and `LITELLM_API_KEY`, or
|
||||
`base_url` and `api_key` under `[model]` in `.agentenv/config.toml`; when the
|
||||
measure says a solve did not run for want of one, tell the person what to
|
||||
set and stop. Read the result's `failures_per_check` and each solve's
|
||||
`failed_checks` before calling a task hard: a check that fails in most failed
|
||||
solves while the app shows the right change is brittle, so fix the check,
|
||||
export and witness again, rather than count it as difficulty.
|
||||
|
||||
## Harder tasks
|
||||
|
||||
When the requester wants tasks worth evaluating or training on, harden a
|
||||
task that came out `too_easy` and measure it again, within the budget. The
|
||||
solver sees and fixes its slips on screen, so one edit with the value in the
|
||||
prompt is solved almost every time; so far, on TreeLine with Sonnet 5.5, so
|
||||
were one more dialog, a conditional move of seven records, and ten
|
||||
corrections read from a second document and applied across the list. Try,
|
||||
one at a time, measuring each:
|
||||
|
||||
- A longer chain of exact edits than that, each a value the checks compare
|
||||
whole, so one slip in the chain fails the task.
|
||||
- Facts read in one view and used in another: values the solver must read
|
||||
from a second document the app opens, or from another record, rather than
|
||||
from the prompt, such as "the same year as The Quiet Glacier's".
|
||||
- Distractors beside each target: records with near-identical names or
|
||||
values, and a guard check on each that it stays as it was.
|
||||
- Mistakes that do not show once saved: a value the app displays rounded or
|
||||
formatted unlike what it stores, or an edit the app keeps only when its
|
||||
field loses focus.
|
||||
|
||||
Stop when the budget left cannot cover 3 solves of the hardened task, and
|
||||
report the bands measured.
|
||||
|
||||
Each hardened task stays sound: its checks still fail on the seed and pass on
|
||||
its end, its near miss fails, and another valid way of doing it passes, so
|
||||
check values, never the order or method the solver used. Put the second
|
||||
document in the seed. Count records with `member_count` and check each
|
||||
value by a selector that names its record, since no check joins two records.
|
||||
|
||||
## Close and report
|
||||
|
||||
`close_build_computer` on every computer still open, also after a failure.
|
||||
If a close fails, call it again; if it still fails, tell the requester what
|
||||
is left.
|
||||
|
||||
Report:
|
||||
|
||||
- the bundle path and the environment name;
|
||||
- each task's name, prompt and end checkpoint;
|
||||
- each check, and why it proves the edit and fails the near miss, and each
|
||||
reader's command;
|
||||
- each task's witness stages with their results, the verdict, and the paths of
|
||||
`witness.json` and the recording;
|
||||
- the seed's contents: `seed_files` and what each holds;
|
||||
- `scrubbed_files` and `unscrubbed_files`; an unscrubbed file still names
|
||||
the host or user, so it needs review before sharing. `recompressed_files`
|
||||
are gzip seeds whose headers were cleared, their content unchanged: give
|
||||
their count;
|
||||
- limitations: the capture's, the export's and anything you could not do;
|
||||
- each measured task's band, passes of finished solves, turns, cost and
|
||||
failures per check, from `difficulty.json`;
|
||||
- the build time and the questions you asked.
|
||||
|
||||
Never publish or share the bundle yourself.
|
||||
@@ -1,399 +0,0 @@
|
||||
# Agent environments from a live application
|
||||
|
||||
Use this when the user asks for an agent environment (an AgentEnv bundle) of a
|
||||
web application: a folder `agent-env` deploys, whose MCP tools answer from what
|
||||
one recording of the application observed and serve the writes that recording
|
||||
confirmed. The workflow records the application once with
|
||||
`capture_browser_scenario`, checks what the environment would serve, records
|
||||
again if a hint names a gap, then writes the bundle with
|
||||
`export_environment_bundle`. For a HAR the user already has, skip to
|
||||
[export](#export-and-hand-off) through `inspect_web_network_capture`, or use
|
||||
`rea environment-build <capture.har> <dir>`. For a task in a desktop
|
||||
application's exported environment, read
|
||||
[its section](#tasks-for-a-desktop-application-environment).
|
||||
|
||||
## Before recording
|
||||
|
||||
- Record only against an instance the user says is disposable or seeded for
|
||||
this purpose: every write step creates, edits or deletes its data. If the
|
||||
user has not said so, ask before any scenario that writes. REA does not
|
||||
start, seed or reset applications.
|
||||
- Credentials come from environment variables the user names. Declare each as a
|
||||
scenario secret, `{secret_id, environment_variable}`; the variable must be set
|
||||
in the environment REA's MCP server started with. Never ask for a password in
|
||||
chat. Declared secrets are redacted as the capture records.
|
||||
- Map the application first with `inspect_web_page` (and `capture_web_screenshot`
|
||||
when layout matters): the login form's locators, and the API calls behind the
|
||||
pages that list and edit the entities the user asked about.
|
||||
- `inspect_web_page` cannot sign in. To see pages behind sign-in, run a short
|
||||
`capture_browser_scenario` that signs in and opens them, with
|
||||
`"capture": {"after_each_step": ["accessibility"]}` (`"dom"` for markup).
|
||||
Read a step's tree with `inspect_analysis_view`, `view: {kind:
|
||||
"browser-scenario", collection: "text", step_index, artifact:
|
||||
"accessibility", offset: 0, limit: 2048}`, paging with `offset`. Capture
|
||||
options are only `after_each_step`, `at_end`, `events`, `network` and
|
||||
`limits`.
|
||||
|
||||
## Record a session an environment can learn writes from
|
||||
|
||||
One `capture_browser_scenario` records the whole session. Start from this
|
||||
shape, with the application's own locators, paths and headers:
|
||||
|
||||
```json
|
||||
{
|
||||
"browser": { "mode": "launch", "executable_path": "/usr/bin/chromium" },
|
||||
"start_url": { "url": "http://127.0.0.1:3000/login" },
|
||||
"secrets": [
|
||||
{ "secret_id": "admin_password", "environment_variable": "APP_PASSWORD" }
|
||||
],
|
||||
"capture": {
|
||||
"network": {
|
||||
"request_body": true,
|
||||
"response_body": true,
|
||||
"header_values": true
|
||||
}
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"step_id": "user",
|
||||
"action": "fill",
|
||||
"timeout_ms": 15000,
|
||||
"locator": { "kind": "css", "selector": "input[name=username]" },
|
||||
"value": { "source": "literal", "value": "admin" }
|
||||
},
|
||||
{
|
||||
"step_id": "password",
|
||||
"action": "fill",
|
||||
"timeout_ms": 15000,
|
||||
"locator": { "kind": "css", "selector": "input[name=password]" },
|
||||
"value": { "source": "secret", "secret_id": "admin_password" }
|
||||
},
|
||||
{
|
||||
"step_id": "sign-in",
|
||||
"action": "press",
|
||||
"timeout_ms": 15000,
|
||||
"locator": { "kind": "css", "selector": "input[name=password]" },
|
||||
"key": "Enter"
|
||||
},
|
||||
{
|
||||
"step_id": "signed-in",
|
||||
"action": "wait_for",
|
||||
"timeout_ms": 15000,
|
||||
"locator": { "kind": "css", "selector": "nav a[href='/settings']" },
|
||||
"state": "visible"
|
||||
},
|
||||
{
|
||||
"step_id": "folders-1",
|
||||
"action": "request",
|
||||
"timeout_ms": 15000,
|
||||
"method": "GET",
|
||||
"path": "/api/folders",
|
||||
"expected_status": [200]
|
||||
},
|
||||
{
|
||||
"step_id": "new-folder-1",
|
||||
"action": "click",
|
||||
"timeout_ms": 15000,
|
||||
"locator": { "kind": "role", "role": "button", "name": "New folder" }
|
||||
},
|
||||
{
|
||||
"step_id": "folder-title-1",
|
||||
"action": "fill",
|
||||
"timeout_ms": 15000,
|
||||
"locator": { "kind": "css", "selector": "input[name=title]" },
|
||||
"value": { "source": "literal", "value": "Scratch folder 1" }
|
||||
},
|
||||
{
|
||||
"step_id": "create-folder-1",
|
||||
"action": "click",
|
||||
"timeout_ms": 15000,
|
||||
"locator": { "kind": "role", "role": "button", "name": "Create" },
|
||||
"save_response": {
|
||||
"method": "POST",
|
||||
"path": "/api/folders",
|
||||
"save": [{ "name": "folder_1", "json_pointer": "/id" }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": "read-folder-1",
|
||||
"action": "request",
|
||||
"timeout_ms": 15000,
|
||||
"method": "GET",
|
||||
"path": "/api/folders/{saved:folder_1}",
|
||||
"expected_status": [200]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Give each step that accepts `timeout_ms` a deadline. Without one, a step waits
|
||||
until it completes or the scenario is cancelled, so a locator that never
|
||||
matches hangs the capture and the client with it. The `wait_for_timeout` pause
|
||||
instead uses `duration_ms` and does not accept `timeout_ms`. Match `wait_for`
|
||||
locators to exactly one element.
|
||||
|
||||
1. Sign in through the UI: `fill` the user and password fields (the password
|
||||
as a secret reference), submit, then `wait_for` an element only a signed-in
|
||||
user sees.
|
||||
2. Read with `request` steps: each collection the environment needs, twice
|
||||
before any write, so fields that change on their own show up; then each
|
||||
object a list shows by id, up to ten per collection. An object known only
|
||||
from a list that leaves out fields its own read shows cannot be read by id
|
||||
or updated in the environment.
|
||||
3. Write the way the application's UI does: fill its forms and click its
|
||||
buttons, so each request carries the fields the UI sends, including ones
|
||||
the server would otherwise derive. Give the click or key press that submits
|
||||
a create `save_response` to keep the new object's ID, then `goto` its page
|
||||
(`/items/{saved:item_1}`) or read it with a `request` step. Never guess an
|
||||
ID, and never create a probe object to learn the next one. Write with
|
||||
`request` steps instead only when the UI cannot reach the operation (an
|
||||
API-only resource, or a UI that does not call the JSON API), or when coverage
|
||||
withholds the UI-shaped write as `unexplained-change` or `contradicted`;
|
||||
that counts as one of the two re-recordings. A `request` step write that
|
||||
coverage withholds that way usually lacks fields the UI sends: record it
|
||||
through the UI instead. If a journey cannot be scripted, leave the write
|
||||
unserved and report the limitation.
|
||||
Record two numbered scratch objects per write type,
|
||||
with varied free text. After each write, and before the next one, read the
|
||||
object back by id and then its list: create 1, read 1, list, create 2,
|
||||
read 2, list; never several writes before their reads.
|
||||
4. Leave more than a second between two creates and between a create and its
|
||||
first edit (`wait_for_timeout` with `duration_ms: 1100`), so lists sorted
|
||||
by creation time show them in order.
|
||||
5. Update both objects and read each back, changing the fields a person
|
||||
would change (a name, an email address, a status, a due date) rather than
|
||||
only a description: a served update accepts only the members a recorded
|
||||
update stored. Run an action that turns something on or off (star,
|
||||
bookmark, archive) twice in a row on one object, reading it back after
|
||||
each: the second call shows whether it sets or flips the field, and the
|
||||
environment serves the action only once a call has shown it. Delete both
|
||||
and read each back (expect 404, or the object marked deleted) and the list.
|
||||
6. Vary what the two creates send, however you write them: the first every
|
||||
field the application takes, the second only what a person must give (its
|
||||
title or name), with the other fields left out or empty. A served create
|
||||
requires each member both creates filled in and lets a caller leave out one
|
||||
a create left out or sent empty, so an agent that names only a title is not
|
||||
refused for fields it had no reason to set.
|
||||
|
||||
About `request` steps:
|
||||
|
||||
- `path` is on the start URL's origin; the page must be on that origin.
|
||||
- Header values are literals, declared secrets, a cookie the browser holds
|
||||
(`{source: "cookie", name}`), a local storage entry
|
||||
(`{source: "local_storage", key, json_pointer}`), a saved value
|
||||
(`{source: "saved", name}`), or Basic credentials
|
||||
(`{source: "basic_credentials", parts: [username, password]}`, sent as the
|
||||
base64 of `username:password` and redacted), with an optional `prefix` such as
|
||||
`"Bearer "` or `"Basic "`. Each credential part is a literal, secret, saved
|
||||
value, cookie or local storage entry, so a header the frontend builds from a
|
||||
login token (the username and the token) can be copied too; the password is
|
||||
a secret or saved value, never a literal. Copy the
|
||||
headers the application's own requests send (a CSRF header such as
|
||||
`X-CSRFToken` from the `csrftoken` cookie, a bearer token). Values read from the page are redacted from records made after they
|
||||
are read, not from the login response that returned them, and values
|
||||
shorter than 8 characters are not redacted at all.
|
||||
- `save: [{name, json_pointer, secret}]` keeps a string or number from the
|
||||
step's JSON response, such as the created object's `/id`, for later steps;
|
||||
`secret: true` redacts it, as for a token the API returns, so mark every
|
||||
saved token, key or session value secret. This also protects the selected
|
||||
source response before its body or headers are retained. Compressed or
|
||||
unknown-size UI responses cannot supply saved values. On a `click` or `press`,
|
||||
`save_response: {method, path, save}` keeps values from the response to the
|
||||
new request observed while the step waits (`{*}` in `path` matches one segment).
|
||||
Matching does not prove UI causality: choose a path distinct from polling;
|
||||
multiple observed matches fail as ambiguous. The step
|
||||
fails unless a 2xx JSON response arrives before its `timeout_ms`. Later
|
||||
steps send a saved value as `{saved:name}` in a request `path` (and its
|
||||
query) or a `goto` URL; in the `json` body, where a string that is only
|
||||
`{saved:name}` keeps the saved JSON type; or as `{source: "saved", name}` in
|
||||
a header or credential part.
|
||||
- Reading a page value makes later bodies costlier to keep: after a CSRF
|
||||
token, bodies over about 47 KiB are kept as `unavailable`. When the API
|
||||
returns larger responses, raise `capture.limits`
|
||||
(`max_network_body_allocation_bytes`, `max_total_event_allocation_bytes`).
|
||||
- Request steps follow no redirects: a redirect fails the step, and
|
||||
`expected_status` cannot name a 3xx status. Use the path the redirect names,
|
||||
such as the one with the trailing slash.
|
||||
- Set `expected_status` on every step, so a refused call fails its step and
|
||||
the later steps report not having run, instead of recording read-backs of
|
||||
nothing.
|
||||
|
||||
Keep it to about 60 to 160 requests. A scenario with no `request` step is
|
||||
recorded exactly as before.
|
||||
|
||||
The capture returns its whole Evidence, every recorded body included, which is
|
||||
often more than a client shows inline. Before checking coverage, confirm that
|
||||
every entry of `normalized_result.steps` has `status: "completed"`; if the
|
||||
client saved the result to a file, search it for `"status":"failed"` and read
|
||||
that step's `error`. A failed sign-in or write cancels every step after it.
|
||||
Use the result's `evidence_id` for coverage and export; do not paste the
|
||||
Evidence back into another tool.
|
||||
|
||||
## Check coverage
|
||||
|
||||
`inspect_analysis_view` with `source: {kind: "retained-evidence",
|
||||
evidence_id}` (the capture's id) and `view: {kind: "environment-coverage",
|
||||
template_rules: ["after-uid-literal", "opaque-token", "sibling-variance",
|
||||
"prefix-variance"]}`. It reports read tools
|
||||
answered from the state or replayed (`reads.recorded_reasons`), the served write
|
||||
tools, and each withheld write with its `reason` and `hint`. It does not apply
|
||||
the serve-time catalog and state size checks; the export reports those.
|
||||
|
||||
Re-record a withheld write at most twice, and only for a recording gap. Stop at
|
||||
once when the reason is application behaviour, and report the write as
|
||||
withheld with its hint:
|
||||
|
||||
| Reason | What to do |
|
||||
| --------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `no-state-target` | Check the path template and recorded views; record missing reads. The route form may not be supported as a mutation target. |
|
||||
| `no-observation` | Re-record: read the entity back by id after the write; read it first for an update or delete. |
|
||||
| `unconfirmed` | Re-record: read the entity and its list after the write. |
|
||||
| `incomplete-entities` | Re-record: read the entity through every view that lists it. |
|
||||
| `unexplained-change` | Re-record once with a second, different call of the same write. |
|
||||
| `no-call` | Re-record once with a write that succeeds. |
|
||||
| `side-effect` | Stop: the application changes other entities. |
|
||||
| `contradicted` | Stop: a later read disagrees with the modelled effect. |
|
||||
| `body-unusable` | Re-record a UI write once with `request` steps (its body was not exposed); else stop. |
|
||||
|
||||
Use new scratch names on each attempt, and delete what an earlier attempt
|
||||
created before recording again (through the UI, or a `request` step with the
|
||||
id that capture shows): the environment starts from what the recording first
|
||||
reads, so a leftover object becomes seeded data that write tasks then find.
|
||||
New browser scenario captures include request times, so a fully timed capture
|
||||
can learn a write clock and serve updates that stamp modification times. Older
|
||||
captures or ones missing a request start keep record order and learn no clock.
|
||||
When coverage reports that limitation, record again with the current version
|
||||
before concluding that the timestamp change cannot be served.
|
||||
|
||||
## Export and hand off
|
||||
|
||||
`export_environment_bundle` with `source: {kind: "retained-evidence",
|
||||
evidence_id}` (or `{kind: "evidence-file", path}`), an absolute
|
||||
`output_directory` that does not exist yet and whose parent does, and a `name`
|
||||
(required when the application runs on `localhost` or an IP address). It returns
|
||||
the coverage again, `tool_count`, the written files and warnings; `dry_run:
|
||||
true` reports without writing. The bundle installs `rea-agents` of this REA's
|
||||
version from the registry; when the user runs an unreleased REA build, pass the
|
||||
`package_tarball` they give you (a packed `rea-agents` tarball) instead.
|
||||
|
||||
Tell the user how many tools answer from the state, which writes are served and
|
||||
which are withheld and why, and the run command (`agent-env run <folder>`). Say
|
||||
that the bundle ships the recorded responses and the capture's local paths
|
||||
(for a browser scenario, the browser executable's path), and
|
||||
list any `credential-named-members` warning pointers for review. Never publish
|
||||
or share the bundle yourself.
|
||||
|
||||
See
|
||||
[agent environments](https://github.com/morluto/rea/blob/main/docs/agent-environments.md)
|
||||
for what the state, replay and the bundle establish, and
|
||||
[evidence workflows](evidence-workflows.md) for the manifest and state views.
|
||||
|
||||
## Tasks for a desktop application environment
|
||||
|
||||
Use this when the user has a folder from `rea capture-desktop-application` and
|
||||
wants a task for the environment `rea export-desktop-application` makes from
|
||||
it: a prompt for a computer-use agent and checks that grade what the agent
|
||||
left in the app's files. You write the prompt and the checks; REA validates
|
||||
the task against the capture and exports it. These commands are CLI only. To
|
||||
build the environment and its task from a request instead, follow
|
||||
[desktop builds](desktop-build.md).
|
||||
|
||||
### Describe the change
|
||||
|
||||
Run `rea describe-desktop-application-task CAPTURE --format json`. Read:
|
||||
|
||||
- `owned_paths`: the only paths whose files the app serves, so the only files
|
||||
a check can read.
|
||||
- `files`: each file there whose state differs between the capture's first
|
||||
and last version, with its `role`, the `baseline` and `final` versions and
|
||||
the net `change`. Text changes list each added and removed line once, and
|
||||
`line_counts` gives lines found at both ends whose count changed.
|
||||
- `values` in each of those files: what the change added, changed and
|
||||
removed, `before` and `after`, at the pointer a check names it by, with
|
||||
`refers_to` naming the element a key such as `category="44"` stands for.
|
||||
Skip entries noted `by_position` or as noise. Only XML files, archives,
|
||||
JSON and key files have values: a text file's or database's change
|
||||
already lists its lines and rows.
|
||||
- `task`: a draft whose `checks` are suggestions from the net change of the
|
||||
documents the user worked on, or of the app's own state when no document
|
||||
changed.
|
||||
- `guards`: optional checks that the rest kept its seed values, such as a
|
||||
like sibling's field or a text file's neighbouring lines; add one or two
|
||||
to show the rest is untouched.
|
||||
- `notes`: why `files` is empty, or why a file's suggestions are thin, such
|
||||
as `by_position` when no attribute names the changed element.
|
||||
|
||||
### Write the prompt
|
||||
|
||||
Say what the user wants done, in their words and the app's terms, naming the
|
||||
app: "In Mousepad, add buy milk to my to-do list and save it". A solving
|
||||
agent's own shell cannot see the computer's files; the app's name sends it to
|
||||
the screen. Never say how the file stores it: no
|
||||
paths the user would not name, line formats, XML, keys or SQL, and never the
|
||||
checks. Replace the draft's placeholder; the export refuses it.
|
||||
|
||||
### Pick the checks
|
||||
|
||||
Checks must express only the observed change:
|
||||
|
||||
- Keep a suggestion only if it says what the user did. Drop those that follow
|
||||
from how the app saves: a format rewrite, renumbered lines, counters. A
|
||||
LibreCAD drawing gaining four holes was rewritten from DXF R12 to 2007, and
|
||||
the draft suggests counts of `SECTION`, `ENDSEC` and `CIRCLE`; only
|
||||
`CIRCLE` equals 9 is the user's change.
|
||||
- Never check unchanged content, whole files, or values the app stamps:
|
||||
times, ids it assigns, window sizes. REA refuses ISO 8601 and `asctime`
|
||||
date-times (`Thu Nov 5 09:20:31 2026`); the
|
||||
rest is your judgment.
|
||||
- Prefer a few robust checks: whole lines over positions, rows by meaningful
|
||||
columns over generated ids, `at_least` when the user may add more.
|
||||
|
||||
Kinds: `file_present` and `file_absent`; `lines_present` and `lines_absent`
|
||||
(`lines`); `line_count` (`exact` or `contains`, and `equals`, `at_least` or
|
||||
`at_most`); `value_equals` and `value_absent` (a JSON `pointer` into the view,
|
||||
such as `/groups/GROUP/KEY` for a key file); `text_equals` and
|
||||
`text_contains` (an XML element's whole text at a `pointer`, optional
|
||||
`normalize_whitespace`); `row_present` and `row_absent` (`table`, `where` by
|
||||
column) and `row_count`. Each names its `file` as a path under the home. A
|
||||
document saved as a zip archive, such as FreeCAD's `.FCStd`, reads as its
|
||||
entries: point into an entry's view, and name array members by selector
|
||||
rather than position, as
|
||||
`/entries/[name=Document.xml]/view/root/children/[name=Object][@name=Hole]`,
|
||||
so the check survives an app version that writes them in another order. A
|
||||
gzip-compressed document, such as Gnumeric's `.gnumeric`, reads as the
|
||||
document inside: point into it as into the same document uncompressed. Give
|
||||
`value_equals` a `tolerance` to compare a number the app writes as text, such
|
||||
as `"5.0000000000000000"`, with a number. Check what the user wrote in a
|
||||
document with `text_equals` on its paragraph element, not `value_equals` on a
|
||||
text node: apps such as AbiWord and Writer split a paragraph into runs as they
|
||||
save, so its first text node holds only part of it; use `text_contains` for a
|
||||
value within a sentence, such as a time. A binary document, such as GIMP's
|
||||
`.xcf`, has no view: give the task a `readers` entry, the app's own headless
|
||||
command printing `REA ` lines, and a check naming it in `reader`, as
|
||||
[desktop builds](desktop-build.md#read-a-binary-file-with-a-reader) shows. The
|
||||
[desktop application tasks](https://github.com/morluto/rea/blob/main/docs/desktop-application-tasks.md)
|
||||
guide has the exact rules.
|
||||
|
||||
### Export the task
|
||||
|
||||
Write the task as JSON with a `name`, the `prompt` and the `checks`, then run
|
||||
`rea export-desktop-application CAPTURE DIRECTORY --task TASK.json`. REA keeps
|
||||
the task only when its checks fail on the seed and all pass on the captured
|
||||
end state. A refusal names the check: one that fails on the end state does not
|
||||
match what the user left; "every check already passes on the seed" means no
|
||||
check captures the change. For several tasks from one capture, repeat
|
||||
`--task` and name each later task's end with `--task-end NAME=CHECKPOINT`.
|
||||
|
||||
When the export refuses an application that no apt repository installs, ask
|
||||
the user for the `.deb` file they installed it from and capture with
|
||||
`--package-file`, then ask where the computer may download that file and pass
|
||||
it as `--package-url`. Never fetch a vendor's download page yourself: such
|
||||
sites often refuse automated downloads, and the copy and its licence are the
|
||||
user's to choose.
|
||||
|
||||
Tell the user the task's name, prompt and checks, and how to measure a solve
|
||||
rate: run `agent-env run DIRECTORY --task NAME --agent claude-code` with
|
||||
`--model MODEL` three times and count the runs reported `passed`. The runs
|
||||
need a model endpoint: `LITELLM_BASE_URL` and `LITELLM_API_KEY`, or
|
||||
`base_url` and `api_key` under `[model]` in `.agentenv/config.toml`. Do not run
|
||||
agents unless asked. The bundle's seed is the user's real data: keep it local.
|
||||
@@ -1,176 +0,0 @@
|
||||
# Evidence, comparison, and verification workflows
|
||||
|
||||
Evidence-producing tools return the complete Evidence record directly in MCP
|
||||
`structuredContent` and CLI JSON. Read `normalized_result` for operation data,
|
||||
and cite `evidence_id` when another tool or your explanation needs a stable
|
||||
reference. Preserve authority, limitations, coverage, and residual unknowns. If a result
|
||||
is incomplete or paginated, continue only when the remaining data matters to
|
||||
the task; do not fetch a bundle or resource merely to read a result already
|
||||
returned inline. When complete `inspect_binary_layout` or
|
||||
`analyze_javascript_application` Evidence is already retained, call
|
||||
`inspect_analysis_view` for a summary, one section/symbol/module, or a stable
|
||||
page instead of repeating the producer.
|
||||
|
||||
For any logical Evidence source that advertises retained references, pass
|
||||
`{kind: "retained-evidence", evidence_id}` instead of copying the complete
|
||||
record back into a call. The same contract covers individual, optional, array
|
||||
and nested sources across native, process, JavaScript, managed, mobile and WASM
|
||||
workflows. Sources can independently mix inline records and exact references.
|
||||
Resolution preserves the source's identity, provenance, coverage, partial facts
|
||||
and unknowns; it neither reruns analysis nor makes incomplete facts complete.
|
||||
Keep domain role and identity requirements: a native dossier cannot substitute
|
||||
for a managed member observation, for example.
|
||||
|
||||
When starting from a summary or inspection page, use its
|
||||
`normalized_result.parent_evidence_id` to reference the complete source.
|
||||
The view's own `evidence_id` names
|
||||
only that view. `compare_artifacts` explicitly supports artifact summary inputs
|
||||
and resolves their parents; do not assume every consumer does. When a workflow
|
||||
follows source links, keep those linked records available too.
|
||||
|
||||
MCP references belong to the current connection's ledger. If the ID is known,
|
||||
pass it directly; no discovery call or provider setup is needed. If it is missing,
|
||||
import the exported bundle or supply complete inline Evidence. Do not retry an
|
||||
unchanged reference or rerun a producer merely to move an existing record.
|
||||
Export before closing the session when the source must survive it. For CLI JSON
|
||||
workflows that advertise `--evidence-bundle`, the same reference JSON resolves
|
||||
against one explicitly selected authenticated bundle:
|
||||
|
||||
```bash
|
||||
rea trace-application-feature request.json --evidence-bundle /absolute/path/evidence.json --json
|
||||
```
|
||||
|
||||
This includes JavaScript traces/comparisons and runtime reconciliation, managed
|
||||
reconstruction/native verification/graph projection, Android/Apple/Harmony graph
|
||||
projection, WASM interface comparison and retained analysis views. The managed
|
||||
member comparison CLI still takes two assembly paths; process comparison takes
|
||||
two capture Evidence JSON paths. Check command help for the installed version.
|
||||
An input bundle grants no implicit file lookup: `evidence-file` and historical
|
||||
graph file selectors are accepted only by contracts advertising them. See the
|
||||
[CLI reuse guide](https://github.com/morluto/rea/blob/main/docs/getting-started/cli.md#import-export-and-compare-evidence).
|
||||
|
||||
Completed `inspect_artifact` inventory also supports a summary and
|
||||
`view: {kind: "inventory", collection: "occurrences", logical_paths: ["package.json", "main.js"]}`.
|
||||
Omit `logical_paths` to page all available occurrences; `[]` selects none.
|
||||
Other inventory collections are `nodes`, `edges` and
|
||||
`integrity_contradictions`. Keep `source_complete` separate from page exhaustion:
|
||||
a missing match in partial source Evidence does not establish absence. Use the
|
||||
returned parent Evidence ID for subsequent views and export before closing the
|
||||
session when the source must survive it.
|
||||
When `tools/list` advertises `inspect_artifact.detail`, request `"summary"` to
|
||||
retain its complete source and receive that view immediately. Without that
|
||||
advertised choice, keep the existing complete call. CLI inspection returns
|
||||
portable complete Evidence; save it before projecting a selected inline view.
|
||||
|
||||
For native procedure/name/string inventories, select `detail: "summary"` when
|
||||
the connected tool advertises it and the task needs counts or selected rows.
|
||||
Use its parent Evidence ID with
|
||||
`view: {kind: "native-inventory", offset: 0, limit: 32, fields: ["value"]}`.
|
||||
Search tools retain the existing pattern/mode/case/document selection. Omit
|
||||
`fields` for complete records; `record.address` and `source_index` remain on
|
||||
selected rows. Selecting `value` keeps its string/decoding qualifications.
|
||||
Parent Evidence ID plus source index preserves same-address
|
||||
alternatives. Follow `coverage.next_offset` only as far as the question needs;
|
||||
exhausting retained rows does not establish complete program discovery.
|
||||
|
||||
`batch_decompile` returns every body unless you select `detail: "summary"` when
|
||||
the connected tool advertises it. Use its parent Evidence ID with
|
||||
`view: {kind: "batch-decompile", collection: "items"}` for body-free rows and
|
||||
exact `where.indices`, `addresses`, `procedure_addresses` or `statuses`. Add
|
||||
`include_pseudocode: true` for several chosen bodies, or use
|
||||
`collection: "pseudocode"` with one row's `index` to page a large body. Batch
|
||||
limitations aggregate every item, so do not attribute them to one successful row.
|
||||
|
||||
Use `record_unknown` to track unresolved questions in the user's investigation
|
||||
and name the authority or environment still required. Supply supporting and
|
||||
contradicting evidence IDs. It updates the session registry directly.
|
||||
Use `update_unknown` with the current revision; reread after a stale revision
|
||||
instead of retrying blindly. Only qualifying observed evidence can verify a
|
||||
resolution.
|
||||
|
||||
Use comparisons with complete, compatible page sets when claiming equivalence
|
||||
or absence:
|
||||
|
||||
- `compare_artifacts` compares inventory pages by occurrence path, content,
|
||||
metadata, and graph relations. Name each side as
|
||||
`{kind: "retained-evidence", evidence_id}` (an `inspect_artifact` result,
|
||||
its nested inventory or its summary) or
|
||||
`{kind: "evidence-file", path, evidence_id?}` instead of resending Evidence.
|
||||
- `compare_functions` compares explicit function Evidence, not fuzzy
|
||||
whole-binary matches. It rejects an `analyze_function` summary reply; pass
|
||||
the summary's `parent_evidence_id` as a `retained-evidence` reference or
|
||||
the complete dossier.
|
||||
- `compare_bundles` compares canonical bundle membership and unknown history.
|
||||
- `find_changed_behavior` combines existing comparisons; static differences
|
||||
remain candidates, not causal proof. Its `comparisons` accept exact references.
|
||||
- `build_call_path` needs explicit function dossiers from one artifact/provider;
|
||||
pass complete parent IDs in `functions`. An incomplete frontier makes absence
|
||||
unknown.
|
||||
- `correlate_static_and_runtime` uses explicit mappings; matching patterns are
|
||||
hypotheses, not causality. Both comparison arrays accept exact references.
|
||||
- `verify_reconstruction` evaluates a finite typed specification. A pass covers
|
||||
only declared comparable claims, not global equivalence.
|
||||
|
||||
Process captures are opt-in behavioral evidence, not a security sandbox. V3
|
||||
captures cannot be upgraded to V4; rerun the original scenario. Distinguish root
|
||||
exit from descendant settlement and require freshness when the task needs it.
|
||||
A timed-out target that must write a final report needs `finalization_ms`: REA
|
||||
sends SIGTERM to the captured root, keeps observing, and sends SIGKILL once
|
||||
only if the target has not exited by the end of the interval.
|
||||
`exit.finalization.signals` lists each attempt with its delivery result; the
|
||||
observed exit signal stays in `exit.signal`, `exit.code` is null for every
|
||||
deadline exit, and the code of a finalizer that exited on its own is in
|
||||
`exit.finalization.exit_code` (null on a signal exit or when no exit was
|
||||
observed).
|
||||
|
||||
Use `inspect_analysis_view` with `view: {kind: "api-surface"}` on retained
|
||||
`inspect_web_network_capture` HAR Evidence to group observed request lines by
|
||||
method, origin and path template. Format matches are inferences, not handler
|
||||
identity or schemas. Group pages cite parent record ordinals; consult record
|
||||
coverage and exclusions before claiming completeness. No requests are replayed.
|
||||
When the connected schema advertises `include_query_parameter_names`, select
|
||||
it in the view for distinct observed names with original record ordinals. Query
|
||||
values are omitted; the names establish neither required parameters nor a
|
||||
complete server schema. Keep the original parent Evidence for exact URLs.
|
||||
|
||||
Use `inspect_analysis_view` with `view: {kind: "environment-manifest"}` on the
|
||||
same retained HAR Evidence to project the primary origin's request lines into
|
||||
named tools with path templates, parameters, observed query names, value-free
|
||||
body shapes and record pointers for an agent environment. Opt into
|
||||
`template_rules` only when recorded identifiers need them; every template,
|
||||
parameter and merged sibling is an inference, no header, cookie or body value is
|
||||
copied, and the three stated unknowns stay unresolved.
|
||||
|
||||
Use `view: {kind: "environment-state"}` with the manifest's `template_rules` to
|
||||
extract the data those reads observed instead: entity types and identity
|
||||
fields, entities with recorded values, documents, and per-tool render plans
|
||||
checked against the recordings. The state comes only from reads before the
|
||||
first related write; types, identity and list membership are inferences. The
|
||||
view also gives each state-changing tool a write plan, served only when a later
|
||||
recorded read confirms its effect and none contradicts it.
|
||||
`rea environment-serve manifest.json --evidence capture.json` derives that
|
||||
state at startup, answers read tools from it, serves the write tools whose plan
|
||||
is served, each applying its effect to that state, and serves `data/reset`,
|
||||
`data/add` and `data/get` on `/agentenv`; other write tools are not served, and
|
||||
each plan states why in its `reason`. It replays the capture instead with
|
||||
`--replay-only` or when the state is unavailable (not derivable, or a catalog
|
||||
over the discovery budget), and its stopped document's `state` says which
|
||||
applied.
|
||||
Use `view: {kind: "environment-coverage"}` with the same rules to see what an
|
||||
environment built from a capture would serve: read tools answered from the
|
||||
state or replayed with their reason, and each write tool served or withheld
|
||||
with a hint about what to record next; it does not apply the serve-time
|
||||
catalog and state size checks. `rea environment-build <capture.har> <dir>`
|
||||
inspects, projects and exports in one step and prints the same coverage, or
|
||||
`null` when the bundle serves replay.
|
||||
|
||||
For a PE/ELF export question, `inspect_native_exports` accepts `detail: "summary"`
|
||||
and retains the complete table. Query `inspect_analysis_view` with
|
||||
`view: {kind: "native-exports", collection: "exports", names: ["selected_name"],
|
||||
fields: ["name"], offset: 0, limit: 32}` and the reported parent Evidence ID.
|
||||
All same-name versions and aliases survive; use `source_index` with the parent ID
|
||||
for stable identity. `collection: "issues"` pages interpretation failures. Page
|
||||
coverage describes selected retained rows; `summary.coverage` or `exports.coverage`
|
||||
still describes the source table, so an empty partial page does not prove absence.
|
||||
CLI `inspect-native-exports --detail summary --evidence_output PATH` saves the
|
||||
complete bundle for later `inspect-analysis-view --evidence-bundle PATH` requests.
|
||||
-266
@@ -1,266 +0,0 @@
|
||||
# JavaScript and Electron application artifacts
|
||||
|
||||
For captured or bundled JavaScript that needs readable source recovery, use
|
||||
`recover_javascript_sources` with an absolute `path` and absent
|
||||
`output_directory`. Supply related runtime/shared/lazy files in
|
||||
`additional_paths` when advertised, then analyze the returned `analysis_input`
|
||||
directory together. Each input retains its own snapshot, digest and byte-range
|
||||
provenance; inputs and recovered code are never executed. Recovery requires a
|
||||
caller-supplied Wakaru on Linux x64; see [the recovery guide](https://github.com/morluto/rea/blob/main/docs/guides/javascript/javascript-recovery.md).
|
||||
|
||||
Use `analyze_javascript_application` directly on the operator-supplied ASAR or
|
||||
extracted tree. When the question concerns known source paths and the connected
|
||||
schema advertises `source_paths`, select exact inventoried paths such as
|
||||
`["package.json", "main.js", "preload.js"]`. Include supporting manifests or
|
||||
source maps explicitly; dependencies are not implicitly analyzed. REA retains
|
||||
the complete artifact inventory and identity but reads source text and analyzes
|
||||
only selected files. The result and summary record `source_selection`; files
|
||||
outside scope and their relationships remain unknown, so partial application
|
||||
coverage does not mean the selected source failed. Use repeated `--source-path`
|
||||
on either static application CLI route, or `--source-paths-json` with a JSON
|
||||
file for selections too long for one command line. Omit the selection when the
|
||||
question requires complete application analysis; `source_paths: []` is invalid,
|
||||
not an unfiltered request. `detail: "summary"` reduces delivery
|
||||
and does not by itself reduce analysis work.
|
||||
|
||||
ASAR header/index admission defaults to 16 MiB / 16777216 bytes;
|
||||
an admission error names `asar.max_header_bytes` and its bounds (at most
|
||||
256 MiB). Call `update_analysis_settings` with that setting, `action: "set"`
|
||||
and enough bytes, then retry the same full-archive request. No inspection or
|
||||
REA restart is required. `REA_ASAR_MAX_HEADER_BYTES` seeds future REA processes.
|
||||
`source_paths`, `detail`, and `max_heap_mb` do not bypass or raise header admission.
|
||||
Runtime JavaScript timeout/heap defaults are also editable; explicit per-call
|
||||
`analysis_timeout_ms` and `max_heap_mb` take precedence.
|
||||
|
||||
Exact `#alias` specifiers resolve through the importer's
|
||||
enclosing package `imports` map to package-local files; nested `node_modules`
|
||||
packages use their own map. Wildcard, recursive, and external-package import
|
||||
targets stay unresolved with that limitation. When size is unknown or large and the connected schema advertises
|
||||
`detail`, request `detail: "summary"`. This returns an `inspect_analysis_view`
|
||||
Evidence record with a distinct ID and a `normalized_result.parent_evidence_id`
|
||||
for the complete retained application analysis. Source-map contents are part of
|
||||
that static analysis. Read findings, coverage, limitations and unknowns, then
|
||||
follow a specific unanswered question. Older servers without `detail` return
|
||||
the complete application Evidence inline when it fits.
|
||||
|
||||
When the connected schema advertises `max_heap_mb` and `analysis_timeout_ms`,
|
||||
these select the isolated source analyzer's old-space heap and per-source
|
||||
deadline. Defaults are 1024 MiB and 300000 ms. They do not change the MCP
|
||||
process heap or the client's tool deadline. For a host timeout without REA
|
||||
output, use the [deadline recovery guide](connection-and-recovery.md#distinguish-client-and-server-deadlines)
|
||||
instead of raising the server budget. A Bun-hosted REA does not enforce the heap choice; on Windows it
|
||||
instead limits the analyzer's committed memory to that choice plus a 512 MiB
|
||||
runtime reserve.
|
||||
Resource-limited analyses preserve completed static facts and
|
||||
report partial semantic coverage with specific unknown scopes; this does not
|
||||
mean the source is malformed. Inspect those limits before retrying a source.
|
||||
When expanded relationships exceed the parent's available memory, inspect the
|
||||
file's modules item view for retained `static_analysis` and
|
||||
`semantic_module_analysis`. Their facts remain available even when graph links
|
||||
are unknown. Cumulative result exhaustion can stop analysis before every file
|
||||
is examined; the inventory and remaining unknown scopes distinguish that case.
|
||||
Timeout or cancellation after source analysis begins retains partial Evidence
|
||||
in the same session. A returned `error.details.partial_observation` reference
|
||||
can be supplied directly to `inspect_analysis_view`. If the SDK cancels locally, retrieve its recorded ID from
|
||||
the Evidence bundle before inspecting the partial analysis.
|
||||
|
||||
## Inventory route candidates without returning the application graph
|
||||
|
||||
When the advertised schema supports `view`, use `detail: "summary"` with
|
||||
`view: {kind: "javascript-routes", collection: "routes", fields: ["path", "confidence"], limit: 20}`.
|
||||
The same view works with `analyze_web_bundle` on already selected captured sources.
|
||||
The complete source Evidence is retained; use its `parent_evidence_id` with
|
||||
`inspect_analysis_view` for `summary`, `routes`, `occurrences`, `sources`, or
|
||||
`unknowns`. Filter and batch IDs before requesting more fields, and follow
|
||||
`coverage.next_offset`. CLI selections require `--evidence_output` to keep the
|
||||
complete record. Static patterns, inferred deployment bases, heuristic system
|
||||
classification, and unavailable sources never establish route reachability or
|
||||
the complete application route denominator. See the
|
||||
[route inventory guide](https://github.com/morluto/rea/blob/main/docs/javascript-route-inventory.md).
|
||||
|
||||
For captured endpoint observations and inference facts, `analyze_web_bundle`
|
||||
also accepts `view: {kind: "web-bundle", collection: "endpoints", limit: 32}`
|
||||
with `detail: "summary"`. Read later `endpoints`, `inferences`, or `summary`
|
||||
pages from the retained parent. Filter before requesting detail; retain
|
||||
`source_index` together with parent ID and collection, and request `location`
|
||||
or `basis` when tracing sources. Page exhaustion does not mean producer
|
||||
completeness: check `completeness`, structured `source_unknowns`, and limitations.
|
||||
Endpoint candidates remain inferences rather than confirmed network behavior.
|
||||
|
||||
## Read a module without repeating analysis
|
||||
|
||||
Use the actual returned parent ID, on the same connection, as `source.evidence_id`.
|
||||
The following IDs are placeholders to replace with returned values. When the
|
||||
module's node ID is not already known, page identities before selecting an item.
|
||||
Pages return only node IDs, paths and roles; choose a useful page size and follow
|
||||
`coverage.next_offset` when another page matters:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": {
|
||||
"kind": "retained-evidence",
|
||||
"evidence_id": "ev_ACTUAL_PARENT_ID"
|
||||
},
|
||||
"view": { "kind": "page", "collection": "modules", "offset": 0, "limit": 16 }
|
||||
}
|
||||
```
|
||||
|
||||
Select the relevant page entry by its exact `node_id` and recorded role:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": {
|
||||
"kind": "retained-evidence",
|
||||
"evidence_id": "ev_ACTUAL_PARENT_ID"
|
||||
},
|
||||
"view": {
|
||||
"kind": "item",
|
||||
"collection": "modules",
|
||||
"selector": { "node_id": "jag_node_ACTUAL_NODE_ID" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Module pages include asset, bundled and source module roles, so one path can
|
||||
identify several entries. Use `selector.path` only when uniqueness is established;
|
||||
the returned node ID avoids needing a complete inventory to prove uniqueness. On ambiguity,
|
||||
select the matching node ID from the page or the error's candidates. Follow
|
||||
`next_offset` when another page matters. An item exposes that module node's
|
||||
recorded observations, identity, hashes, exports and source ranges; it does not
|
||||
include every child syntax observation, related edge or original source file.
|
||||
An absent item fact is not evidence of absence from the whole application.
|
||||
|
||||
For canonical identities and retained source-range counts, use a typed reference
|
||||
page instead of reading every full item:
|
||||
|
||||
```json
|
||||
{
|
||||
"source": {
|
||||
"kind": "retained-evidence",
|
||||
"evidence_id": "ev_ACTUAL_PARENT_ID"
|
||||
},
|
||||
"view": {
|
||||
"kind": "javascript",
|
||||
"collection": "modules",
|
||||
"projection": "references",
|
||||
"where": { "path": "main.js", "kind": "javascript-module" },
|
||||
"offset": 0,
|
||||
"limit": 16
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Filters match exact retained strings; omit a filter to include all values, use
|
||||
`path: null` for unavailable paths, and `path: ""` for a recorded empty path.
|
||||
Rows preserve canonical `identity` and distinct observation support states in
|
||||
`basis`. `retained_source_range_count` counts retained source-range observations,
|
||||
not runtime imports or unique reference sites. Use a row's `node_id` with the item
|
||||
view above for exact locations and full supporting observations. Follow
|
||||
`coverage.next_offset`; page exhaustion describes matching retained rows only.
|
||||
Check `source_coverage` for source selection, partial/unknown graph coverage and
|
||||
unknown counts before drawing a negative conclusion.
|
||||
|
||||
## Trace only the remaining question
|
||||
|
||||
Use `trace_application_feature` on the parent application Evidence for one
|
||||
literal node ID, route, string, API, IPC channel, module, or native export.
|
||||
Select `incoming` for relationships leading to the seed, `outgoing` for those
|
||||
leaving it, and `both` when the question needs both. Trace traversal includes
|
||||
containment relationships, so even a narrow seed can expand many nodes; a
|
||||
bidirectional trace can reach unrelated sibling modules. A module item or page
|
||||
is preferable when its observations already answer the question. Do not treat
|
||||
all reached nodes as participants in the feature. For a broad trace, pass
|
||||
`view: {"kind": "feature-trace", "collection": "paths"}` (or `summary`) to
|
||||
retain the complete trace and read terminal paths first; page `nodes` or `edges`
|
||||
with exact `where.node_ids` through `inspect_analysis_view` as needed.
|
||||
|
||||
Reuse retained parent Evidence when the server advertises it, or supply complete
|
||||
Evidence inline. Include Hopper or Ghidra Evidence only when its artifact digest
|
||||
matches exactly.
|
||||
|
||||
## Recover an oversized display
|
||||
|
||||
An oversized response reports `resource_constraint` and
|
||||
`details.resource: "transport"`. Reuse
|
||||
`details.reported_limits.evidence_reference` with `inspect_analysis_view` for a
|
||||
summary, one module, or a stable page of module identities.
|
||||
`export_evidence_bundle` writes the complete canonical session to a
|
||||
caller-selected file. These workflows keep the original Evidence and coverage;
|
||||
a broad follow-up can also exceed framing.
|
||||
|
||||
A client can truncate a successful result inside its text envelope without a
|
||||
REA `resource_constraint`. Treat a truncation notice or incomplete JSON as a
|
||||
display limitation; do not claim the full record was examined. Reuse the known
|
||||
parent ID for smaller views, or export complete Evidence to a selected file
|
||||
while the session remains open. If the parent ID was lost from the preview,
|
||||
use the prior analysis summary or explicit ledger entries to recover it; do
|
||||
not invent an ID or rerun analysis just to reproduce the same display.
|
||||
|
||||
BrowserWindow preferences, preload and contextBridge surfaces, IPC
|
||||
registrations, utility processes, and native binding requests are static syntax
|
||||
observations. Only a unique exact literal IPC channel match supports an inferred
|
||||
pairing. Dynamic or ambiguous channels remain unresolved. A requested `.node`
|
||||
member is not a verified native export. Never claim runtime reachability,
|
||||
registration, defaults, or policy enforcement from static analysis.
|
||||
|
||||
For version comparison, analyze each version once, then call
|
||||
`compare_application_versions`. Accept only its digest, source-map, structural
|
||||
fingerprint, or non-module semantic matches. Module ordinals and minified names
|
||||
are not persistent identity. Report added or removed only with complete
|
||||
opposite-side coverage; otherwise report unknown.
|
||||
|
||||
When the question asks how one exact exported callable's returned object shape
|
||||
changed, analyze each version once and then call
|
||||
`compare_javascript_export_shapes` with explicit module paths and export names.
|
||||
Use the returned IDs on the same connection, or complete inline Evidence
|
||||
records, from both analysis calls. Accept variant
|
||||
pairing only through the tool's unique exact literal discriminant. Read
|
||||
`property_inventories` and each change's `presence` before treating `unknown` as
|
||||
"the name was not observed": inventories list observed property names even when
|
||||
values stay unknown, with a `source_range` for each paired or unpaired variant.
|
||||
Array holes and mutation-invalidated slots are not observed properties.
|
||||
Complete parent coverage reports presence-only
|
||||
`added`/`removed` independently of unresolved values. Cite the
|
||||
comparison Evidence and report JSON Pointer changes; dynamic values, ambiguous
|
||||
variants, and incomplete parent-property coverage stay unknown. This is static
|
||||
inference, not runtime behavior. When runtime semantics are needed, run
|
||||
behavioral probes against the relevant application versions and capture them
|
||||
through the available browser, Electron, or process workflows.
|
||||
|
||||
## Reusing application Evidence
|
||||
|
||||
When advertised by the connected server, application trace and compare tools
|
||||
accept complete inline Evidence or
|
||||
`{"kind":"retained-evidence","evidence_id":"ev_<64 lowercase hex characters>"}`
|
||||
for their application input (`application`, or `left`/`right`). `inspect_analysis_view`
|
||||
uses the same retained-reference form in `source`, or portable inline Evidence,
|
||||
to project a summary, module page, or one module with its recorded observations.
|
||||
This notation is
|
||||
a template: replace it with the actual returned ID. Versions before 4.1.0 accept only full
|
||||
inline Evidence. Use the exact ID
|
||||
returned by the producer on the same MCP connection. Resolution does not run
|
||||
analysis or select a provider; findings remain inline. For summary or inspection
|
||||
replies, select the complete `normalized_result.parent_evidence_id`. Native
|
||||
observation arrays and runtime reconciliation's `static_layers[].analysis` and
|
||||
`runtime_observations[]` also accept exact references where advertised. Each
|
||||
source can independently be inline or retained, preserving identity, coverage
|
||||
and unknowns. CLI workflows advertising `--evidence-bundle` resolve these inputs
|
||||
from the explicitly selected bundle. `close_binary` releases the engine while
|
||||
retaining references and complete Unknown history on the same MCP connection.
|
||||
`reset_investigation` explicitly clears them; optional `save_path` checkpoints the
|
||||
complete bundle before clearing. Import that bundle in another connection to
|
||||
restore its original references. Retention is bounded, in-memory and never silently
|
||||
evicted; `binary_session.investigation` reports the budget. A missing reference
|
||||
includes its ID and recovery guidance.
|
||||
|
||||
For exact file/native metadata or processing gaps, use
|
||||
`view: {kind: "javascript-facts", where: {paths: ["package.json"]}, property_keys: ["name", "operation", "analysis_status"], limit: 32}`.
|
||||
Rows retain canonical node/observation identity and original Evidence. Paths
|
||||
match Evidence artifact-path/source-range locations; missing properties remain
|
||||
omitted and source coverage stays separate from page exhaustion. Batch paths and
|
||||
select only needed property keys; complete Evidence remains accessible by export.
|
||||
If names are unknown, opt into `include_available_property_keys: true`; it lists
|
||||
the original keys per returned observation without unselected values. Keep each
|
||||
row's node/observation identity paired when composing facts.
|
||||
These rows select application-graph observations; semantic relationships and
|
||||
semantic unknown records remain in the complete parent Evidence and trace tools.
|
||||
@@ -1,931 +0,0 @@
|
||||
# Native, managed, and packaged artifacts
|
||||
|
||||
For release comparisons on one MCP connection, retain `list_procedures` Evidence
|
||||
from each target and reuse its exact IDs after target switches or `close_binary`.
|
||||
For a handoff or new process, export the complete bundle and import it there,
|
||||
including its source closure. `reset_investigation` explicitly clears retained
|
||||
records; optional `save_path` checkpoints them before clearing.
|
||||
`match_native_functions` pairs unique exact names and, when supplied,
|
||||
complete observed body bytes. Reuse optional same-context `list_names`,
|
||||
`procedure_info`, `read_bytes` and existing `analyze_function` dossiers as
|
||||
`left_observations`/`right_observations`; an MCP `analyze_function` summary
|
||||
reply contributes its `parent_evidence_id` as a `retained-evidence` reference,
|
||||
not the summary itself. Avoid decompiling every function merely to match it. Its summary retains a parent for
|
||||
`inspect_analysis_view` `function-matches` collections `matches` and `functions`.
|
||||
Select columns, batch IDs and page individual facts; aliases remain unresolved.
|
||||
Unpaired observations do not prove additions/removals. Existing matched dossier
|
||||
pairs include inputs for `compare_functions`; otherwise analyze just chosen pairs.
|
||||
|
||||
Aggregate completed comparisons with `find_changed_behavior`, selecting each
|
||||
comparison's exact `retained-evidence` ID instead of resending full records.
|
||||
Inline comparison Evidence remains accepted. References belong to the current
|
||||
server: after handoff, import the original bundle including its source closure.
|
||||
Do not substitute dossier IDs or summary views. Static differences remain
|
||||
behavior candidates, with runtime behavior unknown unless separately observed.
|
||||
|
||||
For a matching representation refusal, inspect `details.limit` and raise
|
||||
`native.matching_representation_bytes` with `update_analysis_settings` before
|
||||
retrying the unchanged MCP request. CLI launch seed:
|
||||
`REA_NATIVE_MATCHING_REPRESENTATION_BYTES`. The default is 96 MiB; the policy
|
||||
range is 1 byte to 1 GiB and does not bound process RSS. Keep the complete
|
||||
inventories: splitting them changes global name/body uniqueness.
|
||||
|
||||
## Apple app resources
|
||||
|
||||
Use `open_bundle_resources` with the selected app path, then `inspect_plist`,
|
||||
`decode_interface_builder`, `inspect_asset_catalog`, or `inspect_keyed_archive`.
|
||||
The resource session reads the app without resolving `CFBundleExecutable` or
|
||||
starting a deep provider. Preserve `resource_root` provenance, per-resource
|
||||
hashes where reported, and null executable identity; a directory path is not
|
||||
an executable digest. Export retained Evidence before closing or switching
|
||||
roots. Binary analysis snapshots do not apply. Use `open_binary` to switch to
|
||||
executable analysis, and `close_binary` to release either session kind.
|
||||
|
||||
The four matching CLI commands accept the app path directly. For an archive
|
||||
inside the app, use `inspect-keyed-archive APP_PATH RELATIVE_ARCHIVE_PATH`.
|
||||
Host requirements for plist and compiled asset/Interface Builder tooling still
|
||||
apply; a resource-only session does not establish provider availability.
|
||||
|
||||
## Desktop installation directories
|
||||
|
||||
When an installation directory is supplied and its runtime is unknown, use
|
||||
`inspect_desktop_application` with its absolute path. Default MCP summary
|
||||
delivery retains complete Evidence. Read `files`, `omissions`, `candidates`, `components` or
|
||||
`next_probes` with `inspect_analysis_view` kind `desktop-application`, selecting
|
||||
exact paths, batched file/omission IDs or families and only needed fields and pages.
|
||||
Summary `probe_issues` retains counts and bounded previews. Select files with
|
||||
`has_issues: true` and fields `path`, `read_status`, `issues` to retrieve exact
|
||||
failures directly; preview text is display data, while `file_id` selects full facts. Preserve source
|
||||
coverage, read-range identity and unknowns; directory snapshot identity is not
|
||||
a digest of every file's contents. Component path markers do not establish
|
||||
runtime presence. For compact CLI output, use
|
||||
`inspect-desktop-application PATH --detail summary --evidence_output BUNDLE`.
|
||||
See [desktop installations](https://github.com/morluto/rea/blob/main/docs/desktop-applications.md).
|
||||
|
||||
## Go build metadata
|
||||
|
||||
Use `inspect_go_binary` for an explicit local ELF, PE or Mach-O (universal
|
||||
files need `architecture`) when the
|
||||
question concerns the embedded compiler version, module/dependency versions,
|
||||
local replacements, build settings, or static Go string literals packed without
|
||||
NUL terminators. Pass `include_string_literals: true`
|
||||
(`--include-string-literals`, optional `max_string_literals` /
|
||||
`--max-string-literals`) to recover bounded inferred word-aligned `(ptr, len)`
|
||||
UTF-8 string slices from read-only data and page them through `go`
|
||||
`string_literals` views. No `open_binary`, Go installation or
|
||||
target execution is required. The CLI equivalent is `inspect-go-binary PATH`.
|
||||
Preserve artifact SHA-256, original file ranges, framed module bytes and every
|
||||
unparsed module-text line. A missing record is an unknown, not proof of a
|
||||
non-Go binary; observed metadata does not authenticate its claims, and static
|
||||
`(ptr, len)` string recovery excludes compiler-inlined stack/immediate strings
|
||||
without proving runtime reachability. Function,
|
||||
source and type recovery remain a separate native-analysis question.
|
||||
|
||||
## Model files
|
||||
|
||||
Use `inspect_model_artifact` (`inspect-model-artifact PATH`) for local GGUF v2/v3
|
||||
and safetensors declarations without a model runtime. Header-only mode does not
|
||||
read weights or establish full-file identity. `hash: "full"` explicitly streams
|
||||
all bytes. Retained `model-artifact` views page `metadata`, `tensors` and derived
|
||||
`types`, with exact batch filters and selected fields. Arrays retain typed counts
|
||||
and eight-element previews; omitted values, unknown types and runtime behavior
|
||||
stay explicit. See [model files](https://github.com/morluto/rea/blob/main/docs/model-artifacts.md).
|
||||
|
||||
## IL2CPP metadata
|
||||
|
||||
Use `inspect_il2cpp_metadata` for an explicit local Unity `global-metadata.dat`
|
||||
(versions 24, 27, 29, 31 and 39) when the question concerns assembly images,
|
||||
names, member owners, string literals, namespaces or index ranges. No `open_binary`, Unity installation or target
|
||||
execution is required. The CLI equivalent is `inspect-il2cpp-metadata PATH`.
|
||||
MCP defaults to counts with complete retained Evidence. Query `sections`,
|
||||
`images`, `types`, `methods`, `fields`, `properties`, `events`, `parameters` or
|
||||
`string_literals` through `inspect_analysis_view`; use exact names,
|
||||
batch indices and selected fields. Row indices are scoped by artifact SHA-256
|
||||
and collection; page offsets count filtered matches. Parameters support batch
|
||||
`method_indices` to read one method's parameters directly. Historical reports
|
||||
without member collections retain unknown counts rather than empty inventories. Preserve
|
||||
artifact SHA-256 and index widths. Unrecognized magic may mean encryption and
|
||||
is unsupported, not decrypted. Linking names to `GameAssembly` or
|
||||
`UnityFramework` code addresses remains a separate native-analysis question.
|
||||
See [IL2CPP metadata](https://github.com/morluto/rea/blob/main/docs/il2cpp-metadata.md).
|
||||
|
||||
## Rust dependency metadata
|
||||
|
||||
Use `inspect_rust_binary` for an explicit local ELF, PE, or Mach-O (universal
|
||||
files need `architecture`) when
|
||||
the question concerns the cargo-auditable crate list embedded in `.dep-v0`
|
||||
(Mach-O: `__DATA,.dep-v0`). No `open_binary`, Rust installation, or target
|
||||
execution is required. The CLI equivalent is `inspect-rust-binary PATH`.
|
||||
Preserve artifact SHA-256, package order, dependency indices, and the section
|
||||
file range. A missing section is null metadata, not proof the binary has no
|
||||
dependencies. An omitted package kind stays null. A rustc commit hash is
|
||||
inferred from a read-only `/rustc/<40-hex>/` path and does not by itself
|
||||
identify the compiler. See [Rust binaries](https://github.com/morluto/rea/blob/main/docs/rust-binaries.md).
|
||||
|
||||
## PE Authenticode certificate tables
|
||||
|
||||
Use `inspect_pe_certificates` for an explicit local PE32/PE32+ image when the
|
||||
question concerns the Authenticode certificate table, WIN_CERTIFICATE types, or
|
||||
embedded PKCS#7 signer and X.509 identity fields. No `open_binary`, Windows SDK,
|
||||
or target execution is required. The CLI equivalent is
|
||||
`inspect-pe-certificates PATH`. Preserve artifact SHA-256, table file ranges,
|
||||
payload hashes, original validity-time spellings, and
|
||||
`subject_key_identifier_hex` when the signer uses that form. A missing
|
||||
`IMAGE_DIRECTORY_ENTRY_SECURITY` directory is observed absence of that table,
|
||||
not proof the image is unsigned in every catalog or overlay sense. Signatures,
|
||||
chains, timestamps, and Authenticode digest coverage remain unobserved. See
|
||||
[PE Authenticode certificate tables](https://github.com/morluto/rea/blob/main/docs/pe-certificates.md).
|
||||
|
||||
## PE import and delay-load tables
|
||||
|
||||
Use `inspect_pe_imports` for an explicit local PE32/PE32+ image when the
|
||||
question concerns imported DLL and function names, ordinals, delay-load
|
||||
descriptors, or the pefile import hash. No `open_binary`, Windows SDK, or
|
||||
target execution is required. The CLI equivalent is `inspect-pe-imports PATH`.
|
||||
Preserve artifact SHA-256, directory file ranges, original DLL spellings, and
|
||||
ILT/IAT RVAs. A missing `IMAGE_DIRECTORY_ENTRY_IMPORT` directory is observed
|
||||
absence of that table, not proof the image never imported APIs through another
|
||||
table or overlay. Bound snapshots, loaded IAT contents, and export-forward
|
||||
resolution remain unobserved. See
|
||||
[PE import and delay-load tables](https://github.com/morluto/rea/blob/main/docs/pe-imports.md).
|
||||
|
||||
## Windows Shell Link shortcuts
|
||||
|
||||
Use `inspect_windows_shortcut` for an explicit local `.lnk` file when the
|
||||
question concerns the recorded target path, arguments, working directory, icon
|
||||
location, volume serial, network share names, ExtraData blocks, or Tracker
|
||||
droid identifiers. No `open_binary`, Windows SDK, COM, or target execution is
|
||||
required. The CLI equivalent is `inspect-windows-shortcut PATH`. Preserve
|
||||
artifact SHA-256, FILETIME raw decimals, IDList type bytes, and ExtraData
|
||||
signatures. Unknown IDList items and ExtraData signatures remain raw with
|
||||
SHA-256; they are not guessed. Version-1 UUID MAC and timestamp values are
|
||||
labelled derived. A missing optional section is observed absence of that
|
||||
structure. See
|
||||
[Windows Shell Link shortcuts](https://github.com/morluto/rea/blob/main/docs/guides/native/windows-shortcut.md).
|
||||
|
||||
## PE TLS callbacks and load configuration
|
||||
|
||||
Use `inspect_pe_load_config` for an explicit local PE32/PE32+ image when the
|
||||
question concerns code that runs before the entry point through TLS callbacks,
|
||||
or the security cookie, SafeSEH, CFG, EH-continuation and XFG declarations in
|
||||
the load configuration. No `open_binary`, Windows SDK, or target execution is
|
||||
required. The CLI equivalent is `inspect-pe-load-config PATH`. Preserve artifact
|
||||
SHA-256, directory and callback-array file ranges, and callback VAs as
|
||||
hexadecimal strings. Fields outside the declared `Size` are `null`. Guard-flag
|
||||
mitigations carry `evidence_kind: "inferred"`; enforcement remains unobserved.
|
||||
See
|
||||
[PE TLS callbacks and load configuration](https://github.com/morluto/rea/blob/main/docs/pe-load-config.md).
|
||||
|
||||
## Binary hardening
|
||||
|
||||
Use `inspect_binary_hardening` for an explicit local ELF, PE, or Mach-O file
|
||||
when the question concerns PIE or ASLR, NX, RELRO, stack-protector or FORTIFY
|
||||
signs, control-flow flags, or stripped state. No `open_binary`, pwntools, or
|
||||
target execution is required. The CLI equivalent is
|
||||
`inspect-binary-hardening PATH`. Universal Mach-O files return one image per
|
||||
slice. A missing import is `unknown`, not proof the protection was omitted.
|
||||
Authenticode and Mach-O code-signature rows report presence, not validity.
|
||||
`hardened_runtime` stays unknown and names `inspect_signature`. See
|
||||
[binary hardening](https://github.com/morluto/rea/blob/main/docs/binary-hardening.md).
|
||||
|
||||
## ELF note tables
|
||||
|
||||
Use `inspect_elf_notes` for an explicit local ELF32/ELF64 image when the
|
||||
question concerns PT_NOTE/SHT_NOTE owners, GNU build-id, or GNU ABI-tag
|
||||
fields. No `open_binary` or target execution is required. The CLI equivalent
|
||||
is `inspect-elf-notes PATH`. Preserve artifact SHA-256, table file ranges,
|
||||
descriptor hashes, and original owner spellings. A missing table is observed
|
||||
absence of those declarations, not proof the image never carried notes in
|
||||
another container. See
|
||||
[ELF note tables](https://github.com/morluto/rea/blob/main/docs/elf-notes.md).
|
||||
|
||||
## ELF and Mach-O initializer functions
|
||||
|
||||
Use `inspect_native_initializers` for an explicit local ELF or Mach-O image
|
||||
when the question is which functions run before `main` or at exit: ELF
|
||||
`DT_PREINIT_ARRAY`, `DT_INIT`, `DT_INIT_ARRAY`, `DT_FINI`, `DT_FINI_ARRAY` and
|
||||
legacy `.ctors`/`.dtors`, or Mach-O `__mod_init_func`, `__init_offsets` and
|
||||
`__mod_term_func`. Pass `architecture` for a universal Mach-O. The CLI
|
||||
equivalent is `inspect-native-initializers PATH`. PIE slots are resolved
|
||||
through RELATIVE, RELR and section relocations, and Mach-O slots through
|
||||
chained fixups. Keep each entry's `resolution`: `unresolved` means the value was
|
||||
not established, and `unresolved_import` names an external symbol. Page with
|
||||
`offset` and `limit`. See
|
||||
[ELF and Mach-O initializer functions](https://github.com/morluto/rea/blob/main/docs/guides/native/native-initializers.md).
|
||||
|
||||
## ELF unwind function ranges
|
||||
|
||||
Use `inspect_elf_unwind_ranges` for an explicit local ELF32/ELF64 image when
|
||||
the question concerns function starts and lengths that survive `strip`, which
|
||||
functions have an LSDA, or which ranges have no exact symbol. No
|
||||
`open_binary` or target execution is required. The CLI equivalent is
|
||||
`inspect-elf-unwind-ranges PATH`. Preserve artifact SHA-256, FDE start and
|
||||
end, containing section, exact symbol name, CIE index, LSDA, and personality.
|
||||
Page later ranges with `offset` and `limit`. A missing `.eh_frame` is
|
||||
observed absence of that declaration and of `PT_GNU_EH_FRAME`, not proof the
|
||||
image has no functions. Header disagreement is a limitation and does not drop
|
||||
decoded ranges. `.debug_frame` is reported present or absent and is not
|
||||
decoded. CFA instructions are counted and not evaluated. Indirect EH pointers
|
||||
are not dereferenced. See
|
||||
[ELF unwind function ranges](https://github.com/morluto/rea/blob/main/docs/guides/native/elf-unwind-ranges.md).
|
||||
|
||||
## Apple IPS crash reports
|
||||
|
||||
Use `inspect_apple_ips` for an explicit local Apple `.ips` file when the
|
||||
question concerns crash metadata, exception and termination facts, threads,
|
||||
usedImages, lossless registers, or documented crash classes. No `open_binary`,
|
||||
Xcode, atos, or target execution is required. The CLI equivalent is
|
||||
`inspect-apple-ips PATH`. Preserve artifact SHA-256, `bug_type` as a string,
|
||||
uppercase image UUIDs, and integer literals that exceed IEEE-754 range. Only
|
||||
`bug_type` `"309"` yields a crash body; other DiagnosticReports types return
|
||||
no crash result. Opened Mach-O and dSYM UUID joins remain unobserved. See
|
||||
[Apple IPS crash reports](https://github.com/morluto/rea/blob/main/docs/apple-ips.md).
|
||||
|
||||
## Linux kernel modules
|
||||
|
||||
Use `inspect_kernel_module` for an explicit local ELF32/ELF64 relocatable
|
||||
object when the question concerns `.modinfo` license, vermagic, alias,
|
||||
depends, or parm strings, the `__versions` symbol CRCs, or an appended module
|
||||
signature trailer. No `open_binary`, Linux host, or `modinfo` program is
|
||||
required. The CLI equivalent is `inspect-kernel-module PATH`. Preserve
|
||||
artifact SHA-256, key order, duplicate keys, CRC hex, and section file ranges.
|
||||
A missing section is observed absence when section names are known, not proof
|
||||
the module never carried that metadata. `.gnu.linkonce.this_module` is reported
|
||||
as present or absent; `struct module` stays unknown because its layout changes
|
||||
with the kernel version. `__ksymtab` records are not decoded. The PKCS#7 range
|
||||
and its SHA-256 are observed; certificate, signer, and digest coverage remain
|
||||
unvalidated. An absent trailer is not proof the module is unsigned in another
|
||||
container. gzip, xz, and zstd whole-file containers are unsupported and name
|
||||
`external_decompression` as the next probe: use an external codec utility to
|
||||
decompress into a separate file, then call `inspect_kernel_module` on that file. See
|
||||
[Linux kernel modules](https://github.com/morluto/rea/blob/main/docs/kernel-modules.md).
|
||||
|
||||
## Linux kernel build config
|
||||
|
||||
Use `inspect_kernel_build_config` for an explicit local kernel image (`vmlinux`
|
||||
ELF, raw arm64 `Image`, x86 `bzImage`/`zImage`, or `configs.ko`) when the
|
||||
question is which kernel version it is or how it was configured. No
|
||||
`open_binary`, Linux host, or `extract-ikconfig` is required. The CLI
|
||||
equivalent is `inspect-kernel-build-config PATH`; pass `name` for one exact
|
||||
`CONFIG_*` option and `offset`/`limit` to page the rest. Preserve the banner
|
||||
version, builder, and compiler, the config SHA-256 and option count, and each
|
||||
entry's value kind (`y`, `m`, `string`, `number`, `not_set`). A config is
|
||||
accepted only when `IKCFG_ST` is immediately followed by a valid gzip member
|
||||
and `IKCFG_ED`, so an ordinary `IKCFG_ST` string is not a config. Offsets with
|
||||
`space: "inflated_payload"` are positions in an inflated bzImage payload, not
|
||||
file offsets. An absent config describes only the searched bytes; the kernel
|
||||
may have been built without `CONFIG_IKCONFIG` or with it as a module. Truncated
|
||||
payloads are incomplete, not absent. A framed blob that does not decode, has a
|
||||
bad CRC, or holds non-config text is `malformed`, not absent. xz, lz4, lzma, zstd, bzip2, and lzo
|
||||
payloads are unsupported and name `external_decompression`: decompress with an
|
||||
external utility, then call `inspect_kernel_build_config` on the result. See
|
||||
[Linux kernel build config](https://github.com/morluto/rea/blob/main/docs/guides/native/kernel-build-config.md).
|
||||
|
||||
## CPython pyc files
|
||||
|
||||
Use `inspect_python_pyc` for an explicit local CPython 3.7–3.13 `.pyc` when the
|
||||
question concerns the header magic, PEP 552 source hash or timestamp, nested
|
||||
code-object names, or bytecode identity. No `open_binary`, Python installation,
|
||||
or target execution is required. The CLI equivalent is `inspect-python-pyc PATH`.
|
||||
Preserve artifact SHA-256, the 16-byte header fields, nested code names, and
|
||||
bytecode SHA-256. Bytecode is not decompiled. A decoded header does not prove
|
||||
the matching `.py` is present. See [CPython pyc inspection](https://github.com/morluto/rea/blob/main/docs/python-pyc.md).
|
||||
|
||||
## Erlang BEAM files
|
||||
|
||||
Use `inspect_erlang_beam` for an explicit local `FOR1`/`BEAM` file when the
|
||||
question concerns IFF chunks, atom names, exports, imports, lambdas, or the
|
||||
Code header. No `open_binary`, Erlang installation, or bytecode execution is
|
||||
required. The CLI equivalent is `inspect-erlang-beam PATH`. Preserve artifact
|
||||
SHA-256, chunk file ranges, 1-based atom indices, and original atom spellings.
|
||||
Missing optional tables stay null. For a few facts, use `detail: "summary"` with
|
||||
a `beam` view selecting a collection, exact names/indices and fields; read later
|
||||
pages from `parent_evidence_id` or the complete Evidence bundle. Same-name
|
||||
exports retain every arity and original atom index. CLI summary delivery requires
|
||||
`--evidence_output`. Null table counts remain distinct from decoded empty tables. See
|
||||
[Erlang BEAM files](https://github.com/morluto/rea/blob/main/docs/erlang-beam.md).
|
||||
|
||||
## SQLite snapshots
|
||||
|
||||
Use `inspect_sqlite_database` with an explicit local database snapshot for its
|
||||
schema and optional rows from one exact ordinary table. It requires Node.js
|
||||
24.x >=24.15 or 26+ with the native `DatabaseSync.limits` API. It also consumes the
|
||||
existing sibling `-wal`; retain both original digests when citing its result.
|
||||
Default output is a compact inventory; a selected table expands only its own
|
||||
schema and samples 20 rows. Select `columns` to avoid irrelevant or large cells,
|
||||
and request `schema_scope: "database"` only for complete schema details.
|
||||
Choose `row_limit` when a finite sample is useful and keep truncation and
|
||||
unspecified row order explicit. Virtual tables, shadow tables and views are
|
||||
schema facts, not row-read targets. REA inspects private copies and leaves
|
||||
the source files untouched; obtain a coherent quiescent database/WAL snapshot
|
||||
before inspection. See [SQLite snapshots](https://github.com/morluto/rea/blob/main/docs/guides/data/sqlite-databases.md).
|
||||
|
||||
## Recorded Linux crashes
|
||||
|
||||
Use `inspect_recorded_crash` with an explicit Linux x86-64 ELF core path to
|
||||
inspect historical thread registers, signals, raw notes and original file
|
||||
ranges. It is independent of the active disassembler target. Source-bound
|
||||
register values and note bytes are observations; missing notes, signal-thread
|
||||
association and current executable/library identity remain unknown. PIDs are
|
||||
historical metadata and never authorize live attach or control.
|
||||
|
||||
The optional `include_debugger_context` facet adds mapping candidates through
|
||||
caller-supplied GDB/pwndbg in an owned core-only session. Display names do not
|
||||
establish current file identity; zero reported flags leave permissions unknown.
|
||||
Requested unavailable context fails with setup guidance. See
|
||||
[recorded crashes](https://github.com/morluto/rea/blob/main/docs/guides/native/recorded-crashes.md)
|
||||
for exact upstream profiles, bounds and verification coverage.
|
||||
|
||||
## Offline ELF layout
|
||||
|
||||
Use `inspect_binary_layout` with an explicit local ELF path for file-backed
|
||||
sections, symbols, relocations, linkage names, and static mitigation inferences
|
||||
without opening a disassembler target. When that complete record is already
|
||||
retained, call `inspect_analysis_view` for a summary, mitigations or linkage
|
||||
facet, one section or symbol, or a stable page. Do not re-run the decoder to
|
||||
read a single object.
|
||||
|
||||
## Offline WASM
|
||||
|
||||
Use `inspect_wasm_artifact` with caller-supplied WABT for selected local bytes.
|
||||
For counts, validation and WAT identity, select `detail: "summary"` on the first
|
||||
MCP call. The complete original Evidence stays retained; use
|
||||
`normalized_result.parent_evidence_id` for exact exports or text pages. Omitted
|
||||
or complete detail keeps the existing complete result.
|
||||
For a completed result, use `inspect_analysis_view` with its reported retained
|
||||
reference for a summary or `view: {kind: "wasm", facet: "exports", offset: 0, limit: 32}`.
|
||||
Facets also include sections, imports, candidates, glue, WAT, headers, details
|
||||
and tool profile. Text offsets count UTF-16 code units; follow `next_offset`.
|
||||
The complete source remains retained after an oversized MCP reply. No target
|
||||
execution or network acquisition is implied, and WAT is decoded representation.
|
||||
|
||||
## Offline APK signatures
|
||||
|
||||
Use `inspect_apk_signature` for a selected local APK/ZIP/AAB. Default MCP summaries
|
||||
retain complete Evidence. Select `view.collection: signers/digests/lineage`,
|
||||
with scheme or ID batches, fields and paging; use `inspect_analysis_view` kind
|
||||
`apk-signature` for retained reads after deletion. IDs retain artifact/scheme
|
||||
scope and signer links; `fields: []` keeps identity links. Empty pages preserve
|
||||
partial decoding reasons and unknown authenticity. CLI compact delivery needs
|
||||
`--evidence_output` to preserve the complete parent.
|
||||
It reads the APK
|
||||
Signing Block (v2/v3/v3.1) and v1 `META-INF` signing without executing anything,
|
||||
and reports each signer's certificate facts (for v1, the certificate the PKCS#7
|
||||
block's SignerInfo names), the declared v3 rotation lineage,
|
||||
the declared source-stamp certificate and, for SHA-256/SHA-512 digests, whether
|
||||
the declared content digest matches the current bytes. That match is not a
|
||||
signature verification: anyone who changes the bytes can recompute the digest.
|
||||
`posture` summarises v1-only signing (which cannot tell a v1-only APK from one
|
||||
whose v2/v3 block was removed), observed signer agreement across schemes,
|
||||
whether the v1 signer equals the block-scheme signer (`false` is notable),
|
||||
whether every differing signer appears in a declared (unverified) rotation
|
||||
lineage, and the strongest scheme.
|
||||
It never claims authenticity: no signature or lineage is cryptographically
|
||||
verified.
|
||||
|
||||
## Native targets
|
||||
|
||||
After `open_binary`, use focused search, procedure, or function tools directly.
|
||||
Use `binary_overview` when target metadata or inventory context helps answer the
|
||||
question. Prefer literal search, names, decompilation, callers, callees, and
|
||||
cross-references. Addresses and recovered pseudocode are analysis observations,
|
||||
not original source. Provider unavailability and unsupported metadata remain
|
||||
unknown rather than false.
|
||||
|
||||
For large pseudocode, request `procedure_pseudo_code` with `detail: "summary"`
|
||||
on the first MCP call. It retains the complete observation and returns the
|
||||
original selection, text availability and length without the body. Use
|
||||
`normalized_result.parent_evidence_id` with `inspect_analysis_view` and
|
||||
`view: {kind: "native", facet: "pseudocode", offset: 0, limit: 20000}`;
|
||||
follow `coverage.next_offset` for later pages. Choose a smaller limit if one
|
||||
page exceeds the client or transport budget. Offsets and limits count UTF-16
|
||||
code units, not lines or tokens. Pages reuse the same observation without
|
||||
repeating decompilation. Null and empty text remain distinct. Omitted or
|
||||
`complete` detail preserves full inline delivery. Summary delivery requires a
|
||||
retaining session and does not reduce the work or memory of decompilation.
|
||||
|
||||
For a completed `analyze_function` dossier, use `inspect_analysis_view` with
|
||||
`view: {kind: "native", facet: "value_flow_operations", offset: 0, limit: 32}`
|
||||
and the exact retained Evidence ID to read selected facts without reanalysis.
|
||||
The default MCP reply is already a summary of that dossier: its
|
||||
`normalized_result.parent_evidence_id` is the retained ID to page, and its
|
||||
null counts mark facets the provider did not record or declared unavailable.
|
||||
Native facets also include procedure, pseudocode, assembly, basic blocks,
|
||||
comments, callers/callees, references, unresolved calls, referenced strings and
|
||||
names, the native API record, and value-flow summaries. Pseudocode offsets and limits count UTF-16
|
||||
code units; use the returned `next_offset` for continuation. Provider limitations
|
||||
and unavailable facts remain explicit; the complete parent Evidence is retained.
|
||||
|
||||
`inspect_macho` on a large framework returns megabytes of imports and exports.
|
||||
Select `detail: "summary"` for slice identity, UUID and per-collection counts,
|
||||
then page rows with `view: {kind: "macho", facet: "exports", offset: 0, limit: 64}`
|
||||
and `normalized_result.parent_evidence_id`. Facets are entrypoints,
|
||||
architectures, build_metadata, load_commands, dependencies, imports, exports,
|
||||
segments and provenance. `source_exhaustive: false` means unreturned symbols
|
||||
may exist.
|
||||
|
||||
To see which functions or Objective-C methods a Mach-O actually calls in one
|
||||
run, use `observe_native_calls` with explicit breakpoints and a bounded
|
||||
`duration_ms`/`max_events`. It launches a new process under LLDB, so the target
|
||||
runs and may change files or use the network. Hardened-runtime targets need the
|
||||
`get-task-allow` entitlement.
|
||||
Function breakpoints use exact LLDB runtime names: pass an `inspect_macho` export
|
||||
or import `runtime_name` (`main`), not its symbol-table `name` (`_main`). REA does
|
||||
not translate breakpoint names. Set `module` to the image
|
||||
file name when selecting that image only; otherwise loaded system images also
|
||||
match. Inspect retained locations and module paths before attributing a call.
|
||||
|
||||
Use `binary_session` with no arguments to check the open target, selected
|
||||
provider, and alignment. Its default summary `result.tool_availability` carries
|
||||
the complete tool inventory grouped by current status. Expand each group's
|
||||
`names` and inherit the inventory-level client requirements unless a group
|
||||
overrides them; availability, reasons, remediation, and modes remain inline.
|
||||
Provider candidates keep availability and target-support status, code, and
|
||||
reason. `omitted_fields` names what the summary leaves out; `{"detail":
|
||||
"complete"}` adds each unavailable candidate's `remediation`, provider
|
||||
diagnostics, candidate capability sets, and tool surface, `effects`, and
|
||||
`annotations`. Those arrays name true flags from fixed
|
||||
vocabularies, so an omitted known flag is false. `tools/list` remains the
|
||||
canonical source for full tool schemas when target or provider state changes.
|
||||
|
||||
Provider capability groups carry provider identity and ordered limitations.
|
||||
Prepend each set's `shared_limitations` to a group's `limitations` to recover the
|
||||
operation's full ordered list. Candidate capabilities, returned with complete
|
||||
detail, describe possible providers. Top-level session capabilities describe the current route, and
|
||||
`analysis_provider_binding` identifies the selected provider.
|
||||
|
||||
## Continue editable Ghidra analysis across processes
|
||||
|
||||
For a task spanning agent sessions, check `binary_session` after selecting
|
||||
Ghidra: the current provider's `capabilities[].shared_limitations` states whether
|
||||
Program checkpoints are enabled or the session is ephemeral. Successful native
|
||||
queries distinguish initial analysis from an actually restored packed Program;
|
||||
do not infer that restoration happened merely because checkpoint mode is enabled.
|
||||
|
||||
In ephemeral mode, choose a private absolute directory and set
|
||||
`REA_GHIDRA_CHECKPOINT_DIR` in the environment starting REA. Restart the MCP server
|
||||
after changing that launch configuration; no current tool call or runtime
|
||||
analysis-setting update enables it. Stock ELF/PE/Mach-O imports on Linux/macOS
|
||||
are supported; see the [checkpoint setup and limitations](https://github.com/morluto/rea/blob/main/docs/getting-started/installation.md#automatically-checkpoint-an-owned-ghidra-program).
|
||||
|
||||
With the setting enabled, normal opens restore compatible saved Programs
|
||||
automatically. Initial completed analysis is saved, and `close_binary` saves
|
||||
later metadata edits; no separate save/restore tool is needed. Repair a reported
|
||||
save failure and retry close. Cancellation or process failure can lose edits
|
||||
after the last successful save; unfinished initial analysis cannot resume.
|
||||
Keep the same checkpoint configuration for later processes. Evidence/Unknown
|
||||
history and immutable REA query snapshots have separate persistence workflows.
|
||||
|
||||
## Reuse caller-selected Ghidra analysis
|
||||
|
||||
When the caller supplies an expanded project, configure `REA_GHIDRA_PROJECT_PATH`
|
||||
(the `.gpr` marker) and optionally `REA_GHIDRA_PROGRAM_NAME` (one root program),
|
||||
then restart/reconnect before `open_binary`. CLI uses the same configuration.
|
||||
REA opens only a fingerprint-bound owned copy with `-process -noanalysis -readOnly`;
|
||||
original project files are not opened by the engine or saved. Database edits,
|
||||
import overrides and repair scripts are unavailable in this mode. Treat original
|
||||
analysis completeness/settings as unknown and prior edits as caller-project state.
|
||||
The program's recorded import digest must match the target; changed project content
|
||||
requires reopening for a fresh profile. See the
|
||||
[setup and preparation guide](https://github.com/morluto/rea/blob/main/docs/getting-started/installation.md#reuse-an-existing-ghidra-project).
|
||||
|
||||
## Ghidra startup deadline recovery
|
||||
|
||||
`provider_timeout` during the first Ghidra query can cover import, auto-analysis,
|
||||
bridge startup, and readiness. The default 330,000 ms is configurable through
|
||||
`REA_GHIDRA_STARTUP_TIMEOUT_MS`; a smaller query still needs the same cold import.
|
||||
|
||||
When the error names `ghidra.startup_timeout_ms`, call
|
||||
`update_analysis_settings` with `action: "set"` and a larger value within the
|
||||
reported bounds. Resolve retained cleanup before the next startup attempt.
|
||||
No REA restart is needed; an already accepted startup keeps its original
|
||||
budget. Follow the error's session-specific restart guidance, and preserve useful Evidence before closing
|
||||
and reopening the target when remediation requires that lifecycle step.
|
||||
|
||||
For a per-function timeout, update `ghidra.decompile_timeout_seconds` and retry
|
||||
the same function on the existing Program. Explicit `null` removes that
|
||||
per-function deadline. Inspection is optional when the error supplies the
|
||||
setting and bounds. Runtime updates disappear when REA exits; environment
|
||||
changes only seed future REA processes.
|
||||
|
||||
The client's tool-call deadline must allow startup plus the requested query;
|
||||
server settings and progress notifications cannot override it. CLI analysis
|
||||
avoids the MCP client deadline, but retains the provider deadline. For example:
|
||||
|
||||
```sh
|
||||
REA_GHIDRA_STARTUP_TIMEOUT_MS=1800000 rea analyze /absolute/path/to/app.exe --provider ghidra --json
|
||||
```
|
||||
|
||||
Thirty minutes is an example budget, not a completion guarantee. Keep host CPU
|
||||
and memory controls in place. Disable specific expensive analyzers only when
|
||||
their missing coverage is acceptable and retain the resulting limitations.
|
||||
Exported C and embedded resources remain useful independent evidence, but a
|
||||
timeout does not establish complete analysis or absence of code. See the
|
||||
[deadline and client guide](https://github.com/morluto/rea/blob/main/docs/reference/mcp-contracts.md#ghidra-first-query-deadlines-and-recovery).
|
||||
|
||||
## Bridge request ceiling
|
||||
|
||||
Hopper and Cutter share `REA_BRIDGE_MAX_REQUEST_BYTES`. Unset, both sides use
|
||||
16777216 bytes. The payload budget excludes the terminating LF. A decimal
|
||||
integer from 7464 to 536870887 raises or lowers that ceiling. The floor fits
|
||||
the largest bounded symbol-name command. A tight Cutter status still returns
|
||||
its session id and up to 64 completion receipts. Its path is included when it
|
||||
fits, and is unknown when it does not, so an older descriptor path is not
|
||||
paired with the new generation. The maximum is the encoded payload, one byte under Node's string ceiling so
|
||||
the framing LF still fits. Cutter measures, signs, and writes a fitting
|
||||
response in bounded chunks. The bridge searches only the newly read bytes
|
||||
for the framing LF. The client unescapes each socket chunk into one response
|
||||
string before parsing it, so this ceiling does not keep the paged wire frame
|
||||
and another concatenated copy. Invalid values are configuration errors and do not fall back to 16777216. Set the variable in the environment that starts REA, restart REA,
|
||||
and start Hopper or Cutter from that same environment. An out-of-band bridge
|
||||
without the setting stays at 16777216 bytes. The client reports that mismatch
|
||||
during discovery only after the hello proves a live authenticated bridge, and
|
||||
does not send a status request or the command. A stale descriptor is ignored.
|
||||
Discovery fails if any such live ceiling differs, even when another live
|
||||
session matches. A command response discarded for that ceiling stays an
|
||||
unknown execution with `output_truncated`. A hello
|
||||
that never sends its authenticated frame does not hold the accept loop.
|
||||
At most 8 connection threads run; a further accept is closed, and a failure
|
||||
to start a thread does not stop the listener. `completed_connection` is the
|
||||
longest prefix of connections that have terminated. Dense scalar replies are
|
||||
encoded in bounded batches.
|
||||
The 25000 ms Cutter request deadline and the 256-candidate discovery budget
|
||||
still have no runtime override.
|
||||
|
||||
## Selected native facts and call graphs
|
||||
|
||||
For a large `get_call_graph`, or repeated `procedure_address`,
|
||||
`procedure_callees`, `procedure_callers` and `resolve_containing_procedure`
|
||||
queries, select `detail: "summary"` when the first 64 recorded observations
|
||||
suffice. Read further `native-observation` pages from the returned parent Evidence
|
||||
ID. Graph message IDs index that page's exact-text dictionary; keep level,
|
||||
source index, failed nodes, and partial/unknown relationship coverage.
|
||||
Read referenced source context through `native-context` facets `overview`,
|
||||
`analysis-profile`, `limitations` and `result-limitations`. Reuse inspected
|
||||
context/profile values only for the same digest; until caveats are read,
|
||||
interpretation beyond the returned facts remains unknown. Complete Evidence
|
||||
remains retained and exportable. Small complete replies can cost fewer tokens
|
||||
than additional context/page calls; count the completed task and discovery.
|
||||
|
||||
## Static memory and original file bytes
|
||||
|
||||
For Ghidra, `read_bytes` reads initialized analysis memory, including loader
|
||||
fixups; it does not read a running process. Check `returned_bytes` and `complete`
|
||||
before decoding a table. An incomplete read is missing coverage, not a zero-filled
|
||||
tail. Use `list_segments` to inspect the selected block's initialization and address-space
|
||||
facts, and retain unavailable facts as unknown.
|
||||
|
||||
Use `address_to_file_offset` to anchor one observed address to original file
|
||||
bytes. A mapping for the first address does not establish a contiguous mapping
|
||||
for an entire table, and loader fixups can make analysis bytes differ from source
|
||||
bytes. Keep source ranges and analysis-memory observations separate.
|
||||
|
||||
A pointer table can refer to writable slots populated by a constructor rather
|
||||
than directly to immutable resources. An uninitialized block or an observed zero
|
||||
does not establish the slot's later runtime value. Follow `xrefs` and the relevant
|
||||
writer's assembly or pseudocode; distinguish recorded assignments from inferred
|
||||
execution order. Do not replace unreadable bytes with zeros or conclude that a
|
||||
resource is absent without the necessary initialization evidence.
|
||||
|
||||
## DOS MZ with Ghidra
|
||||
|
||||
For an admitted DOS MZ target, use `inspect_native_load_image` to check measured
|
||||
loaded bytes, file mappings, relocations and the entry against the immutable
|
||||
snapshot. Keep `mismatch` and `unsupported` explicit. Verified import covers the
|
||||
reported static image, not runtime DOS or PC-98 hardware behavior.
|
||||
|
||||
For a large load-image report, select `detail: "summary"` and query its
|
||||
`parent_evidence_id` with `inspect_analysis_view` kind `native-load-image`.
|
||||
Choose a collection such as `relocations` or `mappings`, select only needed
|
||||
fields, and page with offset/limit. Original indices preserve same-address
|
||||
alternatives. Null collections remain unrecorded or inapplicable; paging does
|
||||
not upgrade unsupported verification. CLI requires `--evidence_output` for
|
||||
summary delivery so complete source Evidence remains accessible.
|
||||
|
||||
For Ghidra decompilation, `procedure_pseudo_code` also accepts `procedure: "file-offset:0x..."`; CLI `rea decompile <path> --file-offset=0x... --provider ghidra` resolves the source byte offset internally. An unresolved 0x-prefixed numeric procedure selector also falls back to a unique file mapping. Existing symbols and analysis addresses win; multiple mappings fail as ambiguous. Preserve the returned file-offset and address locations and raw selection observations. ELF virtual addresses are distinct from file offsets; never assume an arbitrary offset plus image base is an analysis address.
|
||||
|
||||
Complete function body ranges use inclusive ends and can contain gaps. Do not
|
||||
treat the enclosing span as code.
|
||||
For packed targets, retain the original and separately derived artifact identities;
|
||||
decompiling an unpacking stub does not recover the unpacked program.
|
||||
|
||||
When reading Objective-C dispatch metadata, distinguish class implementation
|
||||
methods from protocol requirements: class methods report null requirement
|
||||
flags, while decoded protocol declarations retain required and optional facts.
|
||||
Property attributes remain raw name/value tokens; use the property's
|
||||
`atomicity` field for the decoded `N` marker or the default atomic behavior.
|
||||
Null class/meta flags, ivar counts, atomicity, or read-only values mean the
|
||||
selected provider did not establish that fact. Symbol names alone do not prove
|
||||
that a class is a root class or has no ivars.
|
||||
|
||||
## Managed PE/CLI
|
||||
|
||||
Start with `inspect_managed_artifact`. REA's canonical managed inspection is
|
||||
execution-free: do not claim it loaded, reflected, executed, or resolved the
|
||||
assembly. Keep managed/native boundaries and unavailable reconstruction facts
|
||||
explicit. A bring-your-own reconstruction oracle is separate from the canonical
|
||||
parser and must not become an implicit setup dependency.
|
||||
|
||||
For identity/classification questions, `inspect_managed_artifact` can explicitly
|
||||
select `detail: "summary"` on the first MCP call. It retains complete metadata
|
||||
and returns a linked overview with full classification/evidence, nullable
|
||||
module/MVID, frameworks, metadata coverage/issues/limitations and returned-row
|
||||
counts. Default complete requests remain unchanged. Read original rows with
|
||||
`inspect_analysis_view`, source `normalized_result.parent_evidence_id`, and
|
||||
`view: {kind: "managed", collection: "attributes", offset: 0, limit: 16}`.
|
||||
Collections are `references`, `resources` and `attributes`. Paging does not
|
||||
repeat parsing. Returned-row exhaustion
|
||||
does not prove complete metadata or absence under partial/unavailable coverage;
|
||||
preserve build-local tokens and their artifact/MVID scope.
|
||||
|
||||
`inspect_managed_members` always returns the assembly-wide `member_refs`, even
|
||||
for one selected type. For questions about a selected type, select
|
||||
`detail: "summary"` with `type_names`; it returns the selection (with unmatched
|
||||
names), coverage and per-collection counts. Page rows with
|
||||
`view: {kind: "managed-members", collection: "call_edges", offset: 0, limit: 64}`
|
||||
over `types`, `exported_types`, `fields`, `methods`, `member_refs`, `call_edges`,
|
||||
`field_accesses` or `user_strings`.
|
||||
Use exact `names` and `declaring_types` filters before paging supported
|
||||
collections. For a compact method lookup, request `fields: ["name",
|
||||
"declaring_type", "signature"]`; omit `body` to avoid anchors. Identity tokens,
|
||||
original `source_index`, module/MVID and source coverage remain. Check
|
||||
`unresolved_filter_rows` before interpreting an empty match as absence.
|
||||
|
||||
### .NET NativeAOT
|
||||
|
||||
NativeAOT binaries contain native machine code, so ordinary CIL decompilers
|
||||
cannot recover the original method bodies. When managed metadata/CIL is absent,
|
||||
classify that as a native-analysis route rather than claiming source recovery.
|
||||
Open the native image with the selected deep provider and inspect functions,
|
||||
strings, calls, and references; describe decompiler output as pseudocode and
|
||||
inference.
|
||||
|
||||
NativeAOT images can be PE/COFF, ELF, or Mach-O executables and shared
|
||||
libraries. Use file bytes and loader metadata rather than suffixes to identify
|
||||
them. For packaged apps, inventory/extract with REA's artifact tools and retain
|
||||
the exact contained image identity. PDB, ELF debug, or dSYM sidecars are
|
||||
additional evidence only when the provider accepts and matches them.
|
||||
|
||||
REA has a built-in read-only snapshot parser for x64 PE targets with .NET 8 RTR
|
||||
9.1 metadata. An optional headless Ghidra adapter adds database annotations and
|
||||
broader metadata recovery on Linux; it requires an explicitly configured
|
||||
NativeAOT JAR. These are separate routes. The built-in parser also runs on
|
||||
Windows Ghidra P0; read frozen literals from its report, because
|
||||
`search_strings` does not include them. See the
|
||||
[NativeAOT guide](https://github.com/morluto/rea/blob/main/docs/providers/ghidra-nativeaot.md)
|
||||
for setup, supported layouts, and host restrictions. Preserve binary identity
|
||||
and distinguish derived metadata from Ghidra database observations. Recovered
|
||||
types and native pseudocode are not original C# source. Windows Ghidra P0 has
|
||||
no database mutation authority.
|
||||
|
||||
## Packages and extraction
|
||||
|
||||
Use `inspect_artifact` for application bundles, archives, ZIP/APK/IPA/MSIX/AppX,
|
||||
ASAR, or DMG inputs when the artifact graph and findings help answer the
|
||||
question. It returns the complete artifact graph inline. Cite graph manifest
|
||||
IDs when using them.
|
||||
|
||||
For an IPA or a macOS `.app`, ZIP, or DMG, pass the inventory Evidence to
|
||||
`project_apple_application_graph`. It reports application roots and nested
|
||||
bundles by path convention: app extensions, XPC services, frameworks, login
|
||||
items, system and driver extensions, and plug-ins. It also lists privileged
|
||||
helpers, launchd plists, symlinks, and each bundle's `info_plist_path` and
|
||||
executable candidates. Roles are path conventions, not parsed plists; read the
|
||||
listed plists with `inspect_plist`.
|
||||
|
||||
After `open_binary` on a `.app` or Mach-O, use `trace_dylib_resolution` to see
|
||||
which file each `@rpath`, `@loader_path` and `@executable_path` load reaches for
|
||||
every executable in the bundle. It also lists missing or weak loads and earlier
|
||||
`@rpath` candidates that are absent. Narrow large bundles with `roots` or
|
||||
`architecture`. System paths stay undetermined because the dyld shared cache
|
||||
provides them.
|
||||
|
||||
Use `extract_artifact` when materialized files from a supported format are needed.
|
||||
It extracts the active target and accepts optional `integrity_policy`; see the [MCP contracts](https://github.com/morluto/rea/blob/main/docs/reference/mcp-contracts.md).
|
||||
It materializes all regular files into a fresh temporary directory chosen by
|
||||
REA. Symlinks and encrypted entries are inventory facts, not extractable files.
|
||||
The result reports the materialized directory; callers do not choose the
|
||||
destination. No separate permission grant is needed.
|
||||
|
||||
For classic iNES cartridges, `open_binary` recognizes the byte signature without
|
||||
an executable architecture. `inspect_artifact` retains header facts and each
|
||||
PRG/CHR file unit's offset, length and SHA-256; its nested inventory's
|
||||
`ines.reconstruction` verifies the actual ordered region bytes against the
|
||||
whole-file digest. Extraction includes the original header, optional trainer
|
||||
and uninterpreted trailing bytes. These file units are not mapper/CPU bank
|
||||
maps. NES 2.0, UNIF, dirty legacy headers and truncated payloads are explicitly
|
||||
refused. No emulator or 6502 analysis is performed. See the
|
||||
[iNES guide](https://github.com/morluto/rea/blob/main/docs/ines-containers.md).
|
||||
|
||||
Native DMG traversal automatically uses a read-only mount on supported macOS
|
||||
hosts, with an owned temporary mount directory and cleanup. It needs no approval
|
||||
flag. Unsupported hosts or mount failures remain explicit limitations; do not
|
||||
claim child inventory when only root identity is available. DMG extraction is
|
||||
unavailable through `extract_artifact`. To access DMG files on macOS, create a
|
||||
read-only mount with your own tools, select files from that mount for analysis,
|
||||
and detach the mount you created when finished. REA's inspection mount is
|
||||
temporary, not a retained export of the image's contents.
|
||||
|
||||
For headerless DOS COM, explicitly open with `format: "dos-com"` (CLI
|
||||
`--target-format dos-com`). Inspect the load image before trusting function
|
||||
analysis: the entry is `0x10100`, file offset zero, with imposed real-mode segment
|
||||
context. PSP/stack/device state remains unmodeled. See [DOS guide](https://github.com/morluto/rea/blob/main/docs/providers/ghidra-dos.md).
|
||||
|
||||
Use `inspect_byte_entropy` to triage high- and low-entropy ranges before extraction.
|
||||
|
||||
For other headerless images (embedded firmware without ELF/Mach-O/PE/MZ
|
||||
headers), open with `format: "raw-binary"`, `architecture` (`x86`, `x86_64`,
|
||||
`arm`, `arm64`, `6502`, or `m68k`), and `entry_point` (`0x` hex). File offset 0 is mapped at
|
||||
that address. Hopper and IDA refuse this interpretation; Ghidra uses
|
||||
`BinaryLoader` with a generic language: big-endian for `m68k`, little-endian
|
||||
otherwise. Big-endian ARM, Thumb-only, and Cortex-M variants are unsupported.
|
||||
`6502` selects NMOS 6502; the entry and complete file mapping must fit within
|
||||
`0x0000..0xffff`. 65C02, 65816, cartridge containers, bank switching, and mapper
|
||||
semantics are unsupported. `m68k` requires an even entry and decodes the 68040
|
||||
superset of the 68000 family; Amiga/Atari containers and vector-table entry
|
||||
discovery are unsupported. Windows Ghidra P0 admits raw images only for `6502`. See [raw binary guide](https://github.com/morluto/rea/blob/main/docs/providers/raw-binary.md).
|
||||
|
||||
Intel HEX, S-record, UF2 and DfuSe files are containers, not raw images:
|
||||
`inspect_artifact` lists address regions (UF2 per familyID, DfuSe per element
|
||||
with its alternate setting), and `extract_artifact` writes each region. Open an
|
||||
extracted region as `raw-binary` with its start address as `entry_point`. UF2
|
||||
numbering gaps leave coverage unknown; a DfuSe suffix CRC mismatch follows
|
||||
`integrity_policy`. See [UF2 and DfuSe](https://github.com/morluto/rea/blob/main/docs/uf2-dfuse.md).
|
||||
|
||||
### Function annotations in Ghidra
|
||||
|
||||
Fresh-import sessions admit annotations; existing-project reuse is read-only.
|
||||
Use `annotate_native_function` for one function name and/or entry comments, with at least one explicit change. Review its annotation readback and refreshed dossier inline. Changes are atomic; empty comments clear them, omitted fields preserve them. Later MCP calls observe edits. Normal close saves them when owned Program checkpoint mode is enabled; ephemeral mode discards them. CLI `annotate-native-function` follows the same setting when closing after its updated result. Select the [checkpoint workflow](#continue-editable-ghidra-analysis-across-processes) before edits that must survive a later process. Original executable bytes are unchanged; edits invalidate immutable snapshots. Windows P0 does not admit database mutations. For naming only, `set_address_name` / `set_addresses_names` also work with Ghidra: they name function entries or data addresses (primary labels) and return compact per-address success booleans instead of a dossier. Ghidra rejects batch addresses that request the same name or a name another selected address holds; `raw_result.failures` gives each failure's reason.
|
||||
|
||||
A provider switch or different target can retire the active Ghidra database and discard
|
||||
its edits, even if the new provider fails on a later call. Same-target,
|
||||
same-profile reopen keeps the active database. Retained annotation Evidence
|
||||
records what happened; it does not restore the edits.
|
||||
|
||||
For `compare_functions`, obtain `analyze_function` Evidence before and after the
|
||||
edit while the MCP session is open; annotation Evidence is not a comparison
|
||||
input. Export the needed Evidence before close if comparison will happen later.
|
||||
Authenticated same-artifact coordinates preserve renamed entries as
|
||||
`matched / same-entry`, with the canonical `entry_address` inline; name and
|
||||
exact-text changes remain separate dimensions. This requires compatible
|
||||
architecture, provider-owned mapping and selected-image commitments. Equal
|
||||
numeric addresses alone cannot establish correspondence; inspect retained
|
||||
source Evidence and coverage when the match remains ambiguous.
|
||||
|
||||
## Offline PE resource inventory
|
||||
|
||||
Use `inspect_pe_resources` with an absolute `path`, or
|
||||
`rea inspect-pe-resources ./selected.exe --json`, for portable PE32/PE32+
|
||||
resource inspection without opening a native target. Preserve returned Evidence:
|
||||
type/name/language IDs and UTF-16 names, original directory/data/payload ranges,
|
||||
code pages, payload hashes and static icon-group references. Same-language icon
|
||||
resolution is explicit; missing references and size mismatch remain visible.
|
||||
Other payloads are opaque, and this operation does not decode images or simulate
|
||||
Windows resource loading. Consult the budgets and supported tree profile in
|
||||
[offline binary diagnostics](https://github.com/morluto/rea/blob/main/docs/guides/native/binary-diagnostics.md).
|
||||
|
||||
## Offline PE and ELF export tables
|
||||
|
||||
Use `inspect_native_exports` with an absolute `path`, or
|
||||
`rea inspect-native-exports ./libexample.so --json`, to read a PE export
|
||||
directory or ELF dynamic symbol table on any host without opening a native
|
||||
target. Ordinal-only, forwarded and hidden-version exports stay distinct;
|
||||
partial or unsupported coverage means the inventory is incomplete, not that
|
||||
exports are absent. Use `inspect_macho` for Mach-O. See
|
||||
[offline binary diagnostics](https://github.com/morluto/rea/blob/main/docs/guides/native/binary-diagnostics.md).
|
||||
|
||||
For exact core-module import/export differences, use `compare_wasm_interfaces`
|
||||
with two complete inspection Evidence records (or current-session retained
|
||||
references) and explicit scope. Preserve unknown types/duplicate names; static
|
||||
interface equality is not runtime compatibility. CLI accepts complete inline
|
||||
Evidence in `rea compare-wasm-interfaces input.json --json`, or exact references
|
||||
with `--evidence-bundle /absolute/path/evidence.json`. Use the complete parent
|
||||
ID when the inspection returned a summary.
|
||||
|
||||
## Godot PCK
|
||||
|
||||
For a standalone PCK, `open_binary` then `inspect_artifact` with
|
||||
`detail: "summary"` retains the complete static inventory. Use the returned
|
||||
`parent_evidence_id` with `inspect_analysis_view` inventory occurrences filtered
|
||||
by exact `logical_paths` or `occurrence_ids`, and join `artifact_id` to nodes
|
||||
filtered by `artifact_ids`. Select only needed fields; identity links remain.
|
||||
Prefer `inspect-artifact --detail summary --evidence_output PATH` for CLI tasks;
|
||||
the same CLI discovers embedded packs via their EOF trailer without opening a
|
||||
native analysis provider. Normal native opening remains unchanged.
|
||||
|
||||
Distinguish declared MD5 from observed SHA-256 and graph/page completeness from
|
||||
`source_coverage`. Encrypted directories and sparse bundles have unknown entry
|
||||
coverage; encrypted members have unknown content identity. An empty filtered
|
||||
page cannot prove absence in unknown coverage. Retained views are historical,
|
||||
not fresh filesystem checks. No decryption, Godot resource decoding, GDScript
|
||||
decompilation or target execution is provided. See
|
||||
[Godot PCK](https://github.com/morluto/rea/blob/main/docs/godot-pck.md).
|
||||
|
||||
UnityFS AssetBundle containers (`.bundle`, `.ab`, YooAsset packages and other
|
||||
byte-identified files) list `CAB-*` SerializedFiles and `.resS` members with
|
||||
header and node facts; none/LZ4/LZ4HC member blocks stream on demand. Bundles
|
||||
declaring asset-bundle encryption keep header facts with unknown directory
|
||||
coverage; LZMA member bytes retain named occurrences with unavailable hashes.
|
||||
Virtual member offsets are not physical carrier locations. Format 6–8 readers
|
||||
cache one decoded block and support retained filtered/paged views. No
|
||||
decryption or SerializedFile object decoding is provided. See
|
||||
[UnityFS](https://github.com/morluto/rea/blob/main/docs/unityfs.md).
|
||||
|
||||
WeChat mini-program packages (`.wxapkg` files with `0xBE`/`0xED` marks) list
|
||||
members with declared names and absolute carrier offsets; member bytes are
|
||||
stored verbatim. `V1MMWX` PC-client packages report `encrypted` directory
|
||||
status with unknown coverage and are refused on extraction; no decryption is
|
||||
attempted. See
|
||||
[wxapkg](https://github.com/morluto/rea/blob/main/docs/wxapkg.md).
|
||||
|
||||
U-Boot legacy uImages use the same inspection/extraction workflows. Prefer a
|
||||
retained summary and selected inventory views for header facts or component
|
||||
IDs. Header addresses/CPU/OS are declarations; payloads remain as stored.
|
||||
Multi-file components retain their aligned physical ranges; the full payload
|
||||
is also materialized. Unknown name text retains its original 32 bytes. See
|
||||
[U-Boot uImage](https://github.com/morluto/rea/blob/main/docs/uimage.md).
|
||||
|
||||
## ESP images
|
||||
|
||||
For an ESP32-family app or bootloader image, `open_binary` then
|
||||
`inspect_artifact` with `detail: "summary"`. `root_format_metadata` holds the
|
||||
observed `header` and `descriptor` (its `kind` is the app or bootloader role)
|
||||
and the derived `chip`, `checksum`, `appended_sha256` and
|
||||
`undescribed_trailing` ranges. Segment rows (`segment-NN.bin`) carry
|
||||
`load_addr`, `classification` (`mapped`, `loaded`, `not-loaded` or `unknown`,
|
||||
per ESP-IDF `should_map`/`should_load`; RAM ranges are not checked) and
|
||||
`source_location`; select them with `inspect_analysis_view` and
|
||||
`logical_paths`. Signature blocks are `unverified`; `public_key_sha256`
|
||||
matches eFuse key digests.
|
||||
|
||||
Unknown `source_coverage` means truncation or undescribed trailing bytes, such
|
||||
as the flash segments of a `--ram-only-header` image; do not infer absence
|
||||
from it. Erased flash is described. Integrity mismatches are facts, not
|
||||
refusals. ESP8266 images are refused, and flash-encrypted images are not
|
||||
recognized. Segments are not disassembled, and RISC-V or Xtensa segments
|
||||
cannot be opened as raw-binary. See
|
||||
[ESP images](https://github.com/morluto/rea/blob/main/docs/esp-image.md).
|
||||
|
||||
## ESP flash images
|
||||
|
||||
A whole-flash dump opens as `esp-flash-image`. `root_format_metadata` holds
|
||||
`partition_table` (its `method`: `default`, `caller` or a derived
|
||||
`scan`), `bootloader` and `apps` ESP image facts, the `otadata` records,
|
||||
`partitions_outside_dump` and `boot_selection`. Every table has a
|
||||
`boot_selection`: its `basis` names the bootloader's branch (`no-otadata`
|
||||
without otadata, `none` when the bootloader rejects the table, `unknown`
|
||||
when a record is missing), and it lists its `assumptions`;
|
||||
`running_partition` is always `unknown`. Each image names its entry
|
||||
`path`. With `detail: "summary"` the root kind is `esp-flash-image-summary`
|
||||
and each image is a compact `summary` (chip, entry point, segment count,
|
||||
classified load ranges, signature block count, descriptor identity,
|
||||
integrity); complete detail keeps every segment. Partition rows
|
||||
(`partitions/<label>.bin`) carry the declared `type`, `subtype`, names,
|
||||
`flash_address`, `size` and flags; `encrypted` is a storage flag, not
|
||||
proof of ciphertext. `written_offset`/`written_end` bound the bytes that
|
||||
are not erased: an app slot whose facts say `erased` can still hold an older
|
||||
image, and a limitation says so. For a dump read from another flash address, call
|
||||
`open_binary` with `format: "esp-flash-image"` and `flash_offset`; an
|
||||
`esp-image` whose `flash_dump` is set names that offset in a next probe; a
|
||||
relocated table can be named with `partition_table_offset`. No table found
|
||||
means unknown, often flash encryption. Extraction writes `nvs_keys` as
|
||||
ordinary data. See
|
||||
[ESP flash images](https://github.com/morluto/rea/blob/main/docs/esp-flash-image.md).
|
||||
|
||||
## Bun standalone executables
|
||||
|
||||
A `bun build --compile` executable stores its module graph in Mach-O
|
||||
`__BUN,__bun`, a PE or ELF `.bun` section, or an appended trailer.
|
||||
`open_binary` then `inspect_artifact` reports root format `bun-standalone` with
|
||||
the carrier, structural layout, module count, entry point, raw flags and
|
||||
compile exec-argv; each module keeps its `/$bunfs/...` source name, loader,
|
||||
encoding, sizes and SHA-256, with sourcemap, bytecode and module-info regions as
|
||||
separate children. Use summary root metadata `entry_point_logical_path` as the
|
||||
exact inventory `logical_paths` selector, then read the selected content node by
|
||||
its artifact ID; the reader owns virtual-name normalization and indexed fallback. `extract-artifact` writes stored contents through the safe
|
||||
output tree; route extracted JavaScript to `analyze_javascript_application`.
|
||||
Layout labels describe record shapes, not a detected compiler version; unknown
|
||||
or ambiguous layouts are unsupported. Bytecode is retained, not decoded, and the
|
||||
executable is never run. See
|
||||
[Bun standalone executables](https://github.com/morluto/rea/blob/main/docs/bun-standalone.md).
|
||||
|
||||
## Deno compile executables
|
||||
|
||||
A `deno compile` executable stores its application in a Mach-O `__SUI,d3n0l4nd`
|
||||
section, an Intel Mach-O `<~sui-data~>` trailer, an ELF `SUI` note of type
|
||||
`0x53554901`, or a PE `RT_RCDATA` resource named `D3N0L4ND`.
|
||||
`open_binary` then `inspect_artifact` reports root format `deno-standalone`
|
||||
with the carrier, entry point, recorded options, and one occurrence per virtual
|
||||
file, including size and SHA-256. Transpiled output, CJS export analysis, and
|
||||
source maps are separate occurrences with the file's `index` and their own
|
||||
`role`. They normally use a `.deno-*` suffix on the file path and move to an
|
||||
indexed path on a collision, so use the reported `logical_path`. Symlink targets
|
||||
stay recorded path parts. A virtual filesystem that cannot be listed leaves
|
||||
coverage unknown.
|
||||
Environment-file values and CA certificate bytes are retained in local Evidence
|
||||
with their counts and byte lengths. The npm snapshot length is reported and not
|
||||
decoded. `extract-artifact` writes
|
||||
stored files through the safe output tree; route extracted JavaScript to
|
||||
`analyze_javascript_application`. Other layouts are unsupported, and the
|
||||
executable is never run. See
|
||||
[Deno compile executables](https://github.com/morluto/rea/blob/main/docs/deno-standalone.md).
|
||||
@@ -1,140 +0,0 @@
|
||||
# Runtime observation
|
||||
|
||||
## Native runtime questions
|
||||
|
||||
Start from the requested observation. State the selected target, input, expected
|
||||
result, failure signal and stopping condition before a run. Reuse static Evidence
|
||||
to choose a function or hypothesis; `trace_native_values` follows recovered
|
||||
static value dependencies and does not measure runtime values. Prefer static
|
||||
inspection or existing capture comparison when it already answers the question.
|
||||
|
||||
| Question | Existing workflow | Host and effects |
|
||||
| -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Which selected native entry is reached, with which integer argument registers? | `observe_native_calls` on the active Mach-O, with exact function/Objective-C breakpoints and a duration/event bound | macOS with LLDB and required debugger access. Launches a new owned process, stops at entries, records and continues; target execution may write files, show UI or use the network. It never attaches to an existing process. Inspect termination and partial coverage after the run. |
|
||||
| What does caller-authored instrumentation observe? | `list_frida_devices` / `list_frida_processes` when discovery is needed; `start_frida_session`, `load_frida_script`, then status/unload/close | Requires compatible installed Frida bindings and selected device access; the real REA verifier covers Linux. Attach selects an existing PID; spawn selects a program and starts it paused until `resume_frida_session`. Scripts execute in the target and can change memory, files, network activity and behavior. This is not passive inspection. |
|
||||
| Which debugger state or controlled execution explains the behavior? | `start_gdb_session` and `gdb_console`, or `start_rizin_debug_session` and `rizin_debug_command` | Persistent debugger workflows are Linux-only. GDB starts a debugger; console commands choose/load/attach/run targets. Rizin debug start launches the selected target and requires a durable `inferior_tty`. Both consoles are unrestricted and can change target memory/registers, run shell commands, write files and use the network. |
|
||||
| How do terminal output, process lifetime and selected files differ between runs? | `capture_process_scenario`, then `compare_process_captures` with compatible recorded inputs | New captures require Linux or macOS and a working native PTY backend; macOS also needs `xcrun`/Swift for ownership inspection. Capture launches an owned command with ordinary user permissions. Observation paths select snapshots, not a filesystem sandbox. Comparing retained captures does not execute targets and is also available on Windows. |
|
||||
| What does an existing desktop window show, or what changes after a declared UI action? | `observe_native_ui`; use `capture_native_ui_scenario` only for requested actions | macOS 14+ with required Screen Recording/Accessibility access. Select an existing PID/window. Observation does not launch or own the app; scenario actions can change its data or trigger network activity, and leave its state as-is. |
|
||||
|
||||
Close each persistent session with its matching tool. `close_frida_session`
|
||||
unloads scripts and detaches; it does not kill the target. A still-paused spawn
|
||||
is resumed only after instrumentation cleanup is confirmed. GDB and Rizin close
|
||||
can detach a live inferior and let it continue only after verifying its durable
|
||||
output terminal. When disposition or cleanup is unresolved, preserve the owned
|
||||
session and reported recovery; do not assume that closing the debugger terminated
|
||||
the target. Process capture and LLDB call observation instead own their launched
|
||||
processes and report their termination/cleanup outcome. Cancellation is not proof
|
||||
that cleanup completed.
|
||||
|
||||
Unrestricted GDB/Rizin commands and relevant start/close operations retire an
|
||||
active binary-analysis session in the same MCP server. Close an active Ghidra
|
||||
session explicitly first; after target effects settle, reopen the artifact before
|
||||
new static analysis. A Cutter bridge command is likewise unrestricted and may
|
||||
affect its GUI/target; it is not a passive native observation substitute.
|
||||
|
||||
To test a static hypothesis across two builds, retain compatible static
|
||||
comparison Evidence and `compare_process_captures` Evidence. Supply explicit
|
||||
finding/dimension mappings to `correlate_static_and_runtime`. It does not ingest
|
||||
arbitrary Frida, LLDB or debugger traces, auto-match names, or prove causality
|
||||
from cochange. Preserve those traces as their own Evidence and leave unobserved
|
||||
behavior unknown. Record truncation, unmatched breakpoints, changed target
|
||||
identity and cleanup uncertainty as limitations, rather than treating no retained
|
||||
event as proof that the behavior never occurs.
|
||||
|
||||
For same-session MCP comparisons, pass each capture inline or reuse it as
|
||||
`{"kind":"retained-evidence","evidence_id":"ev_..."}`. Default comparison
|
||||
keeps capture-local timing as provenance without treating scheduler or observer
|
||||
latency as a behavior change; use an exact `trace_spec` event when timing is part
|
||||
of the analyst question.
|
||||
|
||||
Detailed contracts and prerequisites: [Frida](https://github.com/morluto/rea/blob/main/docs/guides/runtime/frida-instrumentation.md),
|
||||
[GDB/Rizin/Cutter](https://github.com/morluto/rea/blob/main/docs/reverse-engineering-tools.md),
|
||||
[process capture](https://github.com/morluto/rea/blob/main/docs/guides/runtime/process-capture.md),
|
||||
and [native calls/UI](https://github.com/morluto/rea/blob/main/docs/guides/native/native-investigation.md#native-desktop-observation).
|
||||
|
||||
## Browser and Electron
|
||||
|
||||
Supply the existing browser or Electron instance's literal-port loopback CDP
|
||||
endpoint in the request. No separate permission configuration or per-call
|
||||
approval flag is needed. Browser discovery lists eligible HTTP(S) pages;
|
||||
`allowed_origins` optionally filters them. An individual capture defaults to the
|
||||
selected page's current origin. Electron discovery lists eligible local
|
||||
`file://` targets without a configured file-root allowlist.
|
||||
|
||||
Start passive browser work with `list_browser_targets`; start Electron runtime work with
|
||||
`list_electron_targets`. Before an electron-main Inspector attempt, check
|
||||
packaged fuses with `inspect_electron_fuses`. Inspect the target selected for
|
||||
the user's task. Observation with these tools is passive: never claim REA
|
||||
clicked, navigated, evaluated page JavaScript, invoked IPC, or captured prior
|
||||
activity. REA does not contain the page's network.
|
||||
|
||||
Passive captures omit raw header collections, credential header values, storage
|
||||
values, and raw WebSocket/JSON bodies. Selected response metadata, including
|
||||
content encoding, policy values, and Link URLs, is retained. Local URLs retain ordinary query
|
||||
values and fragments; userinfo credentials are removed. Browser accessibility
|
||||
text, console primitives, value-free shapes, script sources, source maps, and
|
||||
storage key names have request-specific capture options. Electron script
|
||||
sources are also optional. Use `capture_web_screenshot` when pixels are needed.
|
||||
Treat scope filtering, truncation, and attach-window coverage as limitations.
|
||||
Page-declared WebMCP tools are untrusted inventory and are never invoked by REA.
|
||||
|
||||
## Who performs the action
|
||||
|
||||
- The user performs it: `list_browser_targets`, then `inspect_web_page` or
|
||||
`observe_web_session` on the selected target.
|
||||
- The agent must perform it: one `capture_browser_scenario` with declared
|
||||
actions. Use connect mode with the loopback CDP endpoint and `target_id` for
|
||||
an existing browser, or launch mode with a caller-selected executable for a
|
||||
temporary profile. Connect mode first navigates the selected page to
|
||||
`start_url`, which can clear unsaved form values and other temporary state.
|
||||
Recreate the state needed for the investigation in the declared actions.
|
||||
Connect mode leaves the external browser open; launch mode owns a temporary
|
||||
browser and profile. Check `browser.cleanup` for the outcome.
|
||||
Request only the DOM, accessibility, or network details needed for the question.
|
||||
The default final capture contains the URL; step outcomes are reported separately.
|
||||
For multi-step flows with trees or events, select `detail: "summary"` and page
|
||||
exact steps, transactions, text slices or events with `inspect_analysis_view` kind `browser-scenario`.
|
||||
Add a wait action for the expected page state before capturing it. Report that
|
||||
REA performed the declared actions.
|
||||
|
||||
Scenario network records have no request initiator stacks. For that evidence,
|
||||
use passive capture while the user performs the action. Listener source locations
|
||||
identify candidate handlers; they do not establish which script sent a particular
|
||||
request. Avoid overlapping passive capture and scenario actions on the same target.
|
||||
|
||||
Focused follow-up tools:
|
||||
|
||||
- `observe_web_execution`: execution coverage during a window; it resets
|
||||
counters and temporarily disables optimization.
|
||||
- `inspect_web_event_listeners`: source locations for listeners registered
|
||||
directly on the first CSS match, without dispatching an event.
|
||||
- `compare_web_captures`: two normalized passive captures or recorded scenarios.
|
||||
- `discover_webmcp_tools`: inspect page-declared WebMCP inventory; never invoke
|
||||
the discovered tools.
|
||||
|
||||
Use `reconcile_javascript_runtime` only after static application Evidence and
|
||||
passive browser/Electron Evidence exist. Prefer captured-source digest identity;
|
||||
report path/digest disagreement as a mismatch and preserve ambiguity. Explicit
|
||||
path mappings are inference inputs and never broaden filesystem or CDP authority.
|
||||
A bundle observed at runtime does not prove every contained module executed.
|
||||
|
||||
## Fiddler Everywhere traffic debugging
|
||||
|
||||
For large saved captures, request `detail: "summary"`, then page retained Evidence
|
||||
with `inspect_analysis_view` and `view: {kind: "capture", detail: "preview", offset: 0, limit: 20}`.
|
||||
Select complete record pages only for needed headers, metadata or payloads; views
|
||||
read the historical parent without repeating capture decoding.
|
||||
|
||||
For selected protobuf/gRPC bodies, keep raw bytes as the source and use
|
||||
`inspect_analysis_view` kind `protobuf`, collection `bodies`, `frames` or
|
||||
`fields`. Batch exact record ordinals and field numbers/IDs, and select
|
||||
`schema_value` or `text` only when needed. Supply a local descriptor set and
|
||||
message type for schema interpretation; compressed payloads remain unknown.
|
||||
|
||||
Inspect native `.saz` snapshots with `inspect_web_network_capture` and
|
||||
`format: "saz"`; encrypted archives are unsupported. For live
|
||||
traffic analysis, connect Everywhere's separate MCP server and use its
|
||||
advertised session tools with an explicit `sessionsSource` (`LiveTraffic` or
|
||||
`AgentCalls`). Preserve existing sessions and filters when inspecting a
|
||||
recorded run. See the [capture and MCP guide](https://github.com/morluto/rea/blob/main/docs/guides/web/web-network-captures.md)
|
||||
for the workflow and Telerik's public setup and traffic-debugging skills.
|
||||
@@ -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
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
"package-lock.json",
|
||||
".claude-plugin/plugin.json",
|
||||
".codex-plugin/plugin.json",
|
||||
".cursor-plugin/plugin.json",
|
||||
"mcp.json",
|
||||
"_reference/**",
|
||||
"third_party/**",
|
||||
".codex-plugin/mcp.json"
|
||||
|
||||
@@ -92,9 +92,8 @@ project. Installation and recovery details are in the setup guide below.
|
||||
|
||||
Setup supports Claude Code, Codex, Cursor, Gemini CLI, Grok Build and
|
||||
[other agents](docs/getting-started/installation.md#supported-agents). See
|
||||
[installation and setup](docs/getting-started/installation.md) for provider configuration,
|
||||
manual MCP registration, Claude Code/Codex plugins, and the Cursor Marketplace
|
||||
plugin.
|
||||
[installation and setup](docs/getting-started/installation.md) for provider configuration and
|
||||
manual MCP registration and Claude Code/Codex plugin installation.
|
||||
|
||||
MCP uses compact input schemas by default to reduce context overhead. Set
|
||||
`REA_MCP_INPUT_SCHEMA_PROFILE=full` in your MCP registration when you need the
|
||||
|
||||
@@ -350,10 +350,8 @@ tag remains in main's ancestry. Keep the released tag unchanged.
|
||||
|
||||
The synchronization must include `.release-please-manifest.json`,
|
||||
`package.json`, both root versions in `package-lock.json`, `CHANGELOG.md`,
|
||||
`server.json`, the Claude Code and Codex plugin manifests
|
||||
(`.claude-plugin/plugin.json` and `.codex-plugin/plugin.json`), the Codex
|
||||
server declaration (`.codex-plugin/mcp.json`), the Cursor Marketplace manifest
|
||||
(`.cursor-plugin/plugin.json`) and server pin (`mcp.json`), both
|
||||
`server.json`, both plugin manifests (`.claude-plugin/plugin.json` and
|
||||
`.codex-plugin/plugin.json`), the Codex server declaration (`.codex-plugin/mcp.json`), both
|
||||
marketplaces (`.claude-plugin/marketplace.json` and
|
||||
`.agents/plugins/marketplace.json`), and the versioned documentation examples. Run
|
||||
`npm run docs:generate`, `npm run docs:check`, and the release configuration
|
||||
|
||||
@@ -3,16 +3,15 @@
|
||||
Run REA, connect your selected agents, and check the tools needed for your task.
|
||||
Setup previews configuration changes and asks for approval before applying them.
|
||||
|
||||
| Task | Section |
|
||||
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| Connect an agent | [Start setup](#start-setup), then [review changes](#review-setup-changes) |
|
||||
| Install a Claude Code/Codex plugin | [Agent plugins and marketplaces](#agent-plugins-and-marketplaces) |
|
||||
| Install the Cursor Marketplace plugin | [Cursor Marketplace](#cursor-marketplace) |
|
||||
| Run unreleased code | [Run REA from a checkout](#run-rea-from-a-checkout) |
|
||||
| Diagnose one task | [Scoped readiness checks](#check-readiness-for-your-task) |
|
||||
| Configure an analysis engine | [Hopper](#hopper), [Ghidra](#ghidra), the [IDA guide](../providers/ida-provider.md), or [Kuna](../kuna-provider.md) |
|
||||
| Update or remove REA | [Maintenance](#diagnose-update-and-remove) |
|
||||
| Resolve a client-specific problem | [Client configuration](client-configuration.md) |
|
||||
| Task | Section |
|
||||
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| Connect an agent | [Start setup](#start-setup), then [review changes](#review-setup-changes) |
|
||||
| Install a Claude Code/Codex plugin | [Agent plugins and marketplaces](#agent-plugins-and-marketplaces) |
|
||||
| Run unreleased code | [Run REA from a checkout](#run-rea-from-a-checkout) |
|
||||
| Diagnose one task | [Scoped readiness checks](#check-readiness-for-your-task) |
|
||||
| Configure an analysis engine | [Hopper](#hopper), [Ghidra](#ghidra), the [IDA guide](../providers/ida-provider.md), or [Kuna](../kuna-provider.md) |
|
||||
| Update or remove REA | [Maintenance](#diagnose-update-and-remove) |
|
||||
| Resolve a client-specific problem | [Client configuration](client-configuration.md) |
|
||||
|
||||
## Start setup
|
||||
|
||||
@@ -1844,10 +1843,7 @@ The repository's single-plugin marketplaces are declared in
|
||||
`.claude-plugin/marketplace.json` and `.agents/plugins/marketplace.json`.
|
||||
Both select an exact `rea-agents` npm version instead of mutable source-checkout
|
||||
contents. Release Please updates their selectors alongside the plugin manifests.
|
||||
Claude Code and Codex do not need plugin-only publication or generated skill
|
||||
files committed to Git. The Cursor Marketplace plugin is the exception in
|
||||
[Cursor Marketplace](#cursor-marketplace): its skill copy is committed because
|
||||
that install is a Git checkout.
|
||||
No plugin-only publication or generated skill files committed to Git are needed.
|
||||
The qualified release archive contains a runtime-only manifest and
|
||||
`npm-shrinkwrap.json` plus bundled runtime dependencies, including PTY packages
|
||||
for each supported OS and CPU; development dependencies and development-only overrides
|
||||
@@ -1888,8 +1884,7 @@ only the registrations and skill locations that `rea setup` manages, so for a
|
||||
plugin-only install `rea doctor --client <id>` and `rea doctor --skill` report
|
||||
`missing` and recommend `rea setup`. Do not follow that remediation. Check the
|
||||
plugin with the client instead (`claude plugin list` and `/mcp` in Claude
|
||||
Code, `codex plugin list` in Codex, Cursor's plugin settings for a Marketplace
|
||||
install).
|
||||
Code, `codex plugin list` in Codex).
|
||||
|
||||
For analysis engines, ask the connected server: have the agent call
|
||||
`binary_session` and read `result.tool_availability`, which reports what the
|
||||
@@ -1912,66 +1907,6 @@ That report describes the terminal's environment, not the plugin server's.
|
||||
The server inherits the client's environment, so a setting such as
|
||||
`GHIDRA_INSTALL_DIR` that exists only in your terminal does not reach it.
|
||||
|
||||
### Cursor Marketplace
|
||||
|
||||
Cursor's Marketplace installs this public repository as one plugin. The
|
||||
install includes both the `reverse-engineer-anything` skill and the `rea` MCP
|
||||
server. REA does not publish an MCP-only "Add to Cursor" install link, because
|
||||
that link would register the server without the skill.
|
||||
|
||||
The manifest is `.cursor-plugin/plugin.json`. Cursor discovers `mcp.json` at
|
||||
the repository root and the skill directory named by `skills`. The committed
|
||||
skill is `.cursor-plugin/skills/reverse-engineer-anything`, a copy of the
|
||||
authored skill at `.agents/skills/reverse-engineer-anything`.
|
||||
`node scripts/generate-cursor-plugin-skill.mjs --check` fails when that copy
|
||||
drifts, including in `npm run docs:check` and package prepack. Marketplace
|
||||
installs are a Git checkout and do not contain `dist/`. The server entry
|
||||
therefore runs the published package instead of a local build:
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "[email protected]", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<!-- x-release-please-end -->
|
||||
|
||||
Release Please updates that pin with `.cursor-plugin/plugin.json` and
|
||||
`package.json`. The skill text can describe capabilities newer than the pinned
|
||||
server until the next release; follow the connected server's tool list. The
|
||||
same pin is what `rea doctor --provider <id>` should use when you inspect an
|
||||
engine from a terminal, as shown above.
|
||||
|
||||
A Marketplace install gives you:
|
||||
|
||||
- the `reverse-engineer-anything` skill and its references
|
||||
- an `rea` MCP server launched with `npx -y rea-agents@<pinned version> mcp`
|
||||
(Cursor shows it as `plugin-rea-rea`)
|
||||
- the shared plugin logo at `assets/rea-plugin.png`
|
||||
|
||||
It does not install analysis engines, put `rea` on `PATH`, or write
|
||||
`~/.cursor/mcp.json`. `rea setup --client cursor` remains the direct
|
||||
registration path and still writes that file plus the setup-owned skill. Use
|
||||
the Marketplace plugin or that registration, not both, so Cursor does not
|
||||
start two `rea` servers.
|
||||
|
||||
Node.js and npm must be available to Cursor. The plugin does not install them.
|
||||
Restart Cursor after the plugin is listed. If tools stay missing, read the
|
||||
plugin server's launch error. `rea doctor --client cursor` does not see
|
||||
Marketplace state and can report `missing`; do not run setup to repair a
|
||||
plugin-only install.
|
||||
|
||||
Publishing the listing is a separate maintainer step at
|
||||
[cursor.com/marketplace/publish](https://cursor.com/marketplace/publish).
|
||||
This repository change does not submit the plugin.
|
||||
|
||||
## MCP Registry
|
||||
|
||||
REA is published in the official MCP Registry as `io.github.morluto/rea`. Registry
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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.
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"[email protected]",
|
||||
"mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-4
@@ -110,9 +110,6 @@
|
||||
".claude-plugin/plugin.json",
|
||||
".codex-plugin/plugin.json",
|
||||
".codex-plugin/mcp.json",
|
||||
".cursor-plugin/plugin.json",
|
||||
".cursor-plugin/skills",
|
||||
"mcp.json",
|
||||
"scripts/plugin-mcp-launcher.mjs",
|
||||
"npm-shrinkwrap.json"
|
||||
],
|
||||
@@ -297,7 +294,7 @@
|
||||
"docs:preview": "vitepress preview docs",
|
||||
"docs:generate": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts npm run docs:prepare",
|
||||
"docs:check": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run docs:check:built && npm run test:local -- tests/conformance/readmeConsistency.test.ts",
|
||||
"docs:check:built": "node scripts/generate-package-metadata.mjs --check && node scripts/generate-skill-metadata.mjs --check && node scripts/generate-cursor-plugin-skill.mjs --check && node scripts/generate-product-catalog.mjs --check && node scripts/generate-completion-ledger.mjs --check && node scripts/check-doc-facts.mjs && node scripts/generate-error-schema.mjs --check && npm run licenses:check",
|
||||
"docs:check:built": "node scripts/generate-package-metadata.mjs --check && node scripts/generate-skill-metadata.mjs --check && node scripts/generate-product-catalog.mjs --check && node scripts/generate-completion-ledger.mjs --check && node scripts/check-doc-facts.mjs && node scripts/generate-error-schema.mjs --check && npm run licenses:check",
|
||||
"docs:error-schema": "node scripts/check-dependency-install.mjs && node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run artifacts:error-schema",
|
||||
"prepack": "node scripts/prepack.mjs",
|
||||
"prepare": "node scripts/prepare.mjs",
|
||||
|
||||
@@ -52,16 +52,6 @@
|
||||
"path": ".codex-plugin/mcp.json",
|
||||
"jsonpath": "$.mcpServers.rea.args[1]"
|
||||
},
|
||||
{
|
||||
"type": "json",
|
||||
"path": ".cursor-plugin/plugin.json",
|
||||
"jsonpath": "$.version"
|
||||
},
|
||||
{
|
||||
"type": "json",
|
||||
"path": "mcp.json",
|
||||
"jsonpath": "$.mcpServers.rea.args[1]"
|
||||
},
|
||||
{
|
||||
"type": "json",
|
||||
"path": ".claude-plugin/marketplace.json",
|
||||
|
||||
@@ -636,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",
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { syncCursorPluginSkill } from "./lib/cursor-plugin-bundle.mjs";
|
||||
|
||||
const arguments_ = new Set(process.argv.slice(2));
|
||||
for (const argument of arguments_)
|
||||
if (argument !== "--check")
|
||||
throw new Error(`Unknown Cursor plugin skill option: ${argument}`);
|
||||
|
||||
await syncCursorPluginSkill({
|
||||
root: join(dirname(fileURLToPath(import.meta.url)), ".."),
|
||||
check: arguments_.has("--check"),
|
||||
});
|
||||
@@ -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 });
|
||||
},
|
||||
]);
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
export const CURSOR_PLUGIN_MANIFEST: ".cursor-plugin/plugin.json";
|
||||
export const CURSOR_PLUGIN_MCP: "mcp.json";
|
||||
export const AUTHORED_SKILL_DIRECTORY: ".agents/skills/reverse-engineer-anything";
|
||||
export const CURSOR_PLUGIN_SKILLS: ".cursor-plugin/skills";
|
||||
export const CURSOR_PLUGIN_SKILL: "reverse-engineer-anything";
|
||||
export const CURSOR_PLUGIN_LOGO: "assets/rea-plugin.png";
|
||||
export const CURSOR_PLUGIN_REPOSITORY: "https://github.com/morluto/rea";
|
||||
|
||||
export interface CursorPluginBundleIssue {
|
||||
readonly field: string;
|
||||
}
|
||||
|
||||
export interface CursorPluginBundleInput {
|
||||
readonly manifest: {
|
||||
readonly name?: string;
|
||||
readonly version?: string;
|
||||
readonly description?: string;
|
||||
readonly author?: { readonly name?: string };
|
||||
readonly repository?: string;
|
||||
readonly license?: string;
|
||||
readonly keywords?: readonly string[];
|
||||
readonly skills?: string;
|
||||
readonly logo?: string;
|
||||
readonly mcpServers?: unknown;
|
||||
};
|
||||
readonly mcp: {
|
||||
readonly mcpServers?: {
|
||||
readonly rea?: {
|
||||
readonly command?: string;
|
||||
readonly args?: readonly string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
readonly packageVersion: string;
|
||||
readonly logoExists: boolean;
|
||||
readonly skillNames: readonly string[];
|
||||
readonly skillFrontmatter: {
|
||||
readonly name?: string;
|
||||
readonly description?: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface CursorPluginInspection {
|
||||
readonly manifest: CursorPluginBundleInput["manifest"] & {
|
||||
readonly version: string;
|
||||
};
|
||||
readonly mcp: {
|
||||
readonly mcpServers: {
|
||||
readonly rea: {
|
||||
readonly command: string;
|
||||
readonly args: readonly string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
readonly packageVersion: string;
|
||||
readonly issues: readonly CursorPluginBundleIssue[];
|
||||
}
|
||||
|
||||
export function cursorPluginSkillPaths(
|
||||
directory: string,
|
||||
prefix?: string,
|
||||
): Promise<readonly string[] | null>;
|
||||
|
||||
export function cursorPluginBundleIssues(
|
||||
input: CursorPluginBundleInput,
|
||||
): readonly CursorPluginBundleIssue[];
|
||||
|
||||
export function syncCursorPluginSkill(options: {
|
||||
readonly root: string;
|
||||
readonly check: boolean;
|
||||
}): Promise<void>;
|
||||
|
||||
export function inspectCursorPluginBundle(
|
||||
root: string,
|
||||
): Promise<CursorPluginInspection>;
|
||||
|
||||
export function cursorMarketplaceInstallLinkIssues(
|
||||
documents: readonly { readonly path: string; readonly text: string }[],
|
||||
): readonly { readonly path: string; readonly marker: string }[];
|
||||
@@ -1,216 +0,0 @@
|
||||
import { mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
import { ensureGeneratedFile } from "./generated-file.mjs";
|
||||
|
||||
// Identity fields follow the shared plugin manifest checked for local Cursor
|
||||
// publication (name, release version, repository, skill, and icon). The
|
||||
// Marketplace checkout has no dist/, so the server is the pinned npm command
|
||||
// instead of the bundled launcher.
|
||||
export const CURSOR_PLUGIN_MANIFEST = ".cursor-plugin/plugin.json";
|
||||
export const CURSOR_PLUGIN_MCP = "mcp.json";
|
||||
export const AUTHORED_SKILL_DIRECTORY =
|
||||
".agents/skills/reverse-engineer-anything";
|
||||
export const CURSOR_PLUGIN_SKILLS = ".cursor-plugin/skills";
|
||||
export const CURSOR_PLUGIN_SKILL = "reverse-engineer-anything";
|
||||
export const CURSOR_PLUGIN_LOGO = "assets/rea-plugin.png";
|
||||
export const CURSOR_PLUGIN_REPOSITORY = "https://github.com/morluto/rea";
|
||||
const GENERATE_COMMAND = "node scripts/generate-cursor-plugin-skill.mjs";
|
||||
const INSTALL_LINK_MARKERS = ["install-mcp", "deeplink/mcp", "mcp-install"];
|
||||
|
||||
const failure = (code, detail) => {
|
||||
const error = new Error(`${code}: ${detail}`);
|
||||
error.code = code;
|
||||
return error;
|
||||
};
|
||||
|
||||
const cursorServerArgs = (version) => ["-y", `rea-agents@${version}`, "mcp"];
|
||||
|
||||
/** Relative file inventory for one skill directory, in stable order. */
|
||||
export const cursorPluginSkillPaths = async (directory, prefix = "") => {
|
||||
const paths = [];
|
||||
let entries;
|
||||
try {
|
||||
entries = await readdir(directory, { withFileTypes: true });
|
||||
} catch (cause) {
|
||||
if (cause?.code === "ENOENT") return null;
|
||||
throw cause;
|
||||
}
|
||||
for (const entry of entries.sort((left, right) =>
|
||||
left.name < right.name ? -1 : left.name > right.name ? 1 : 0,
|
||||
)) {
|
||||
const relativePath = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
||||
if (entry.isDirectory())
|
||||
paths.push(
|
||||
...(await cursorPluginSkillPaths(
|
||||
join(directory, entry.name),
|
||||
relativePath,
|
||||
)),
|
||||
);
|
||||
else if (entry.isFile()) paths.push(relativePath);
|
||||
else
|
||||
throw failure(
|
||||
"cursor_plugin_skill_inventory",
|
||||
`Skill bundle requires regular files or directories: ${relativePath}`,
|
||||
);
|
||||
}
|
||||
return paths;
|
||||
};
|
||||
|
||||
const skillFrontmatter = (content) => {
|
||||
const normalized = content.replaceAll("\r\n", "\n");
|
||||
if (!normalized.startsWith("---\n")) return null;
|
||||
const closing = normalized.indexOf("\n---\n", 4);
|
||||
if (closing === -1) return null;
|
||||
const fields = {};
|
||||
for (const line of normalized.slice(4, closing).split("\n")) {
|
||||
const separator = line.indexOf(":");
|
||||
if (separator === -1) continue;
|
||||
const key = line.slice(0, separator).trim();
|
||||
const value = line.slice(separator + 1).trim();
|
||||
if (key !== "" && fields[key] === undefined) fields[key] = value;
|
||||
}
|
||||
return fields;
|
||||
};
|
||||
|
||||
/**
|
||||
* Structured differences between the Cursor plugin declaration and the
|
||||
* package version it must launch. Empty means the manifest, server pin,
|
||||
* logo, and skill frontmatter agree.
|
||||
*/
|
||||
export const cursorPluginBundleIssues = ({
|
||||
manifest,
|
||||
mcp,
|
||||
packageVersion,
|
||||
logoExists,
|
||||
skillNames,
|
||||
skillFrontmatter: frontmatter,
|
||||
}) => {
|
||||
const issues = [];
|
||||
const add = (field) => issues.push({ field });
|
||||
if (manifest?.name !== "rea") add("name");
|
||||
if (manifest?.version !== packageVersion) add("version");
|
||||
if (typeof manifest?.description !== "string" || manifest.description === "")
|
||||
add("description");
|
||||
if (typeof manifest?.author?.name !== "string" || manifest.author.name === "")
|
||||
add("author.name");
|
||||
if (manifest?.repository !== CURSOR_PLUGIN_REPOSITORY) add("repository");
|
||||
if (typeof manifest?.license !== "string" || manifest.license === "")
|
||||
add("license");
|
||||
if (
|
||||
!Array.isArray(manifest?.keywords) ||
|
||||
manifest.keywords.length === 0 ||
|
||||
manifest.keywords.some((keyword) => typeof keyword !== "string")
|
||||
)
|
||||
add("keywords");
|
||||
if (manifest?.skills !== CURSOR_PLUGIN_SKILLS) add("skills");
|
||||
if (manifest?.logo !== CURSOR_PLUGIN_LOGO) add("logo");
|
||||
if (Object.hasOwn(manifest ?? {}, "mcpServers")) add("mcpServers");
|
||||
if (logoExists !== true) add("logo.file");
|
||||
const server = mcp?.mcpServers?.rea;
|
||||
if (
|
||||
server?.command !== "npx" ||
|
||||
JSON.stringify(server?.args) !==
|
||||
JSON.stringify(cursorServerArgs(packageVersion))
|
||||
)
|
||||
add("mcp.args");
|
||||
if (
|
||||
!Array.isArray(skillNames) ||
|
||||
skillNames.length !== 1 ||
|
||||
skillNames[0] !== CURSOR_PLUGIN_SKILL
|
||||
)
|
||||
add("skills.entries");
|
||||
if (frontmatter?.name !== CURSOR_PLUGIN_SKILL) add("skill.name");
|
||||
if (
|
||||
typeof frontmatter?.description !== "string" ||
|
||||
frontmatter.description === ""
|
||||
)
|
||||
add("skill.description");
|
||||
return issues;
|
||||
};
|
||||
|
||||
/** Copy the authored skill into the Marketplace plugin tree, or fail when it drifted. */
|
||||
export const syncCursorPluginSkill = async ({ root, check }) => {
|
||||
const sourceRoot = join(root, AUTHORED_SKILL_DIRECTORY);
|
||||
const outputRoot = join(root, CURSOR_PLUGIN_SKILLS, CURSOR_PLUGIN_SKILL);
|
||||
const sourcePaths = await cursorPluginSkillPaths(sourceRoot);
|
||||
if (sourcePaths === null)
|
||||
throw failure(
|
||||
"cursor_plugin_skill_inventory",
|
||||
`Authored skill is missing: ${AUTHORED_SKILL_DIRECTORY}`,
|
||||
);
|
||||
if (!check) await rm(outputRoot, { recursive: true, force: true });
|
||||
for (const relativePath of sourcePaths) {
|
||||
const source = (
|
||||
await readFile(join(sourceRoot, relativePath), "utf8")
|
||||
).replaceAll("\r\n", "\n");
|
||||
if (!check)
|
||||
await mkdir(dirname(join(outputRoot, relativePath)), { recursive: true });
|
||||
try {
|
||||
await ensureGeneratedFile({
|
||||
path: join(outputRoot, relativePath),
|
||||
source,
|
||||
check,
|
||||
generateCommand: GENERATE_COMMAND,
|
||||
});
|
||||
} catch (cause) {
|
||||
throw failure(
|
||||
"cursor_plugin_skill_stale",
|
||||
cause instanceof Error ? cause.message : String(cause),
|
||||
);
|
||||
}
|
||||
}
|
||||
const outputPaths = await cursorPluginSkillPaths(outputRoot);
|
||||
if (
|
||||
outputPaths === null ||
|
||||
JSON.stringify(outputPaths) !== JSON.stringify(sourcePaths)
|
||||
)
|
||||
throw failure(
|
||||
"cursor_plugin_skill_inventory",
|
||||
"Cursor plugin skill inventory drifted; run node scripts/generate-cursor-plugin-skill.mjs",
|
||||
);
|
||||
};
|
||||
|
||||
/** Read the committed plugin and report structured contract issues. */
|
||||
export const inspectCursorPluginBundle = async (root) => {
|
||||
const manifest = JSON.parse(
|
||||
await readFile(join(root, CURSOR_PLUGIN_MANIFEST), "utf8"),
|
||||
);
|
||||
const mcp = JSON.parse(await readFile(join(root, CURSOR_PLUGIN_MCP), "utf8"));
|
||||
const packageJson = JSON.parse(
|
||||
await readFile(join(root, "package.json"), "utf8"),
|
||||
);
|
||||
const logoExists = await stat(join(root, CURSOR_PLUGIN_LOGO))
|
||||
.then((metadata) => metadata.isFile())
|
||||
.catch((cause) => {
|
||||
if (cause?.code === "ENOENT") return false;
|
||||
throw cause;
|
||||
});
|
||||
const skillDirectory = join(root, CURSOR_PLUGIN_SKILLS);
|
||||
const skillNames = await readdir(skillDirectory);
|
||||
const skillContent = await readFile(
|
||||
join(skillDirectory, CURSOR_PLUGIN_SKILL, "SKILL.md"),
|
||||
"utf8",
|
||||
);
|
||||
return {
|
||||
manifest,
|
||||
mcp,
|
||||
packageVersion: packageJson.version,
|
||||
issues: cursorPluginBundleIssues({
|
||||
manifest,
|
||||
mcp,
|
||||
packageVersion: packageJson.version,
|
||||
logoExists,
|
||||
skillNames,
|
||||
skillFrontmatter: skillFrontmatter(skillContent),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
/** MCP-only install links omit the skill, so Marketplace docs must not carry them. */
|
||||
export const cursorMarketplaceInstallLinkIssues = (documents) =>
|
||||
documents.flatMap(({ path, text }) =>
|
||||
INSTALL_LINK_MARKERS.filter((marker) => text.includes(marker)).map(
|
||||
(marker) => ({ path, marker }),
|
||||
),
|
||||
);
|
||||
@@ -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),
|
||||
]:
|
||||
|
||||
@@ -35,16 +35,6 @@ const codexServerConfigSchema = z.object({
|
||||
}),
|
||||
}),
|
||||
});
|
||||
const cursorPluginManifest = ".cursor-plugin/plugin.json";
|
||||
const cursorPluginServer = "mcp.json";
|
||||
const cursorServerConfigSchema = z.object({
|
||||
mcpServers: z.object({
|
||||
rea: z.object({
|
||||
command: z.literal("npx"),
|
||||
args: z.tuple([z.literal("-y"), z.string(), z.literal("mcp")]),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
const sectionsSchema = z.object({
|
||||
versioning: z.string().optional(),
|
||||
"changelog-sections": z.array(
|
||||
@@ -250,20 +240,6 @@ async function validateCandidate(git, sha, version, commits) {
|
||||
: manifest;
|
||||
launches.push([path, server.mcpServers.rea.args[1]]);
|
||||
}
|
||||
const cursorManifest = await readJson(
|
||||
git,
|
||||
sha,
|
||||
cursorPluginManifest,
|
||||
versionSchema,
|
||||
);
|
||||
versions.push([cursorPluginManifest, cursorManifest.version]);
|
||||
const cursorServer = await readJson(
|
||||
git,
|
||||
sha,
|
||||
cursorPluginServer,
|
||||
cursorServerConfigSchema,
|
||||
);
|
||||
const cursorPin = cursorServer.mcpServers.rea.args[1];
|
||||
// Catalogs are generated and checked from this candidate by the release job;
|
||||
// they are not version authority stored in the immutable source tree.
|
||||
const generated = await git([
|
||||
@@ -286,13 +262,6 @@ async function validateCandidate(git, sha, version, commits) {
|
||||
}
|
||||
}
|
||||
// The launcher checks the scalar pin against the runtime in its own package.
|
||||
// Compare manifest versions first so a wholly stale candidate reports the
|
||||
// shared version authority before this Marketplace package specifier.
|
||||
if (cursorPin !== `rea-agents@${version}`) {
|
||||
throw new Error(
|
||||
`${cursorPluginServer}@${sha} launches ${cursorPin}; release branch requires rea-agents@${version}.`,
|
||||
);
|
||||
}
|
||||
for (const [location, launched] of launches) {
|
||||
if (launched !== version) {
|
||||
throw new Error(
|
||||
|
||||
@@ -373,8 +373,6 @@ const VERSION_ONLY_PLUGIN_METADATA = new Set([
|
||||
".claude-plugin/plugin.json",
|
||||
".codex-plugin/plugin.json",
|
||||
".codex-plugin/mcp.json",
|
||||
".cursor-plugin/plugin.json",
|
||||
"mcp.json",
|
||||
".claude-plugin/marketplace.json",
|
||||
".agents/plugins/marketplace.json",
|
||||
]);
|
||||
@@ -397,14 +395,9 @@ function pluginMetadataWithoutVersions(path, text) {
|
||||
}
|
||||
if ("version" in manifest) manifest.version = "";
|
||||
const launch = manifest.mcpServers?.rea?.args;
|
||||
// Only the scalar pinned version is release-owned. Blank that version and
|
||||
// keep a package name such as rea-agents@ so a renamed package still counts
|
||||
// as a product change. The executable and path remain product behavior.
|
||||
if (Array.isArray(launch) && typeof launch[1] === "string")
|
||||
launch[1] = launch[1].replace(
|
||||
/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?/u,
|
||||
"",
|
||||
);
|
||||
// Only the scalar pinned version is release-owned; the launcher owns the
|
||||
// fixed package name and the executable/path remain product behavior.
|
||||
if (Array.isArray(launch) && typeof launch[1] === "string") launch[1] = "";
|
||||
return JSON.stringify(manifest);
|
||||
}
|
||||
|
||||
|
||||
@@ -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()]);
|
||||
}
|
||||
@@ -43,10 +43,6 @@ if (await pathExists(join(root, "src"))) {
|
||||
throw new Error("Prepack could not resolve the active package manager");
|
||||
run(process.execPath, [packageManagerPath, "run", "build:cached"]);
|
||||
run(process.execPath, ["scripts/generate-skill-metadata.mjs", "--check"]);
|
||||
run(process.execPath, [
|
||||
"scripts/generate-cursor-plugin-skill.mjs",
|
||||
"--check",
|
||||
]);
|
||||
} else {
|
||||
for (const path of [
|
||||
"scripts/lib/cli-process-environment.mjs",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -266,39 +266,6 @@ export async function verifyPackagePack({ root, workspace, commands }) {
|
||||
)
|
||||
throw new Error(`Packaged plugin ${path} is missing its shared icon`);
|
||||
}
|
||||
const cursorManifest = JSON.parse(
|
||||
(
|
||||
await commands.exec("tar", [
|
||||
"-xOf",
|
||||
tarball,
|
||||
"package/.cursor-plugin/plugin.json",
|
||||
])
|
||||
).stdout,
|
||||
);
|
||||
const cursorServer = JSON.parse(
|
||||
(await commands.exec("tar", ["-xOf", tarball, "package/mcp.json"])).stdout,
|
||||
);
|
||||
const cursorLogo = String(cursorManifest.logo ?? "").replace(/^\.\//u, "");
|
||||
const cursorSkill =
|
||||
"package/.cursor-plugin/skills/reverse-engineer-anything/SKILL.md";
|
||||
if (
|
||||
cursorManifest.version !== packedManifest.version ||
|
||||
cursorManifest.skills !== ".cursor-plugin/skills" ||
|
||||
cursorLogo !== "assets/rea-plugin.png" ||
|
||||
cursorManifest.mcpServers !== undefined ||
|
||||
cursorServer.mcpServers?.rea?.command !== "npx" ||
|
||||
JSON.stringify(cursorServer.mcpServers?.rea?.args) !==
|
||||
JSON.stringify([
|
||||
"-y",
|
||||
`${packedManifest.name}@${packedManifest.version}`,
|
||||
"mcp",
|
||||
]) ||
|
||||
!packedFiles.includes(`package/${cursorLogo}`) ||
|
||||
!packedFiles.includes(cursorSkill)
|
||||
)
|
||||
throw new Error(
|
||||
"Packaged Cursor plugin does not ship the pinned server, skill, and logo",
|
||||
);
|
||||
if (packedFiles.includes("package/.mcp.json"))
|
||||
throw new Error("Plugin metadata must not register a project MCP server");
|
||||
if (!packedFiles.includes("package/.codex-plugin/mcp.json"))
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
@@ -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" },
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
@@ -1010,7 +1010,7 @@ describe("decompile_all partial results", () => {
|
||||
if (!result.ok)
|
||||
expect(result.error.userMessage).toContain("before address start");
|
||||
expect(calls).toHaveLength(0);
|
||||
await expect(readFile(directory())).rejects.toThrow();
|
||||
await expect(stat(directory())).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -910,6 +910,7 @@ describe("inspectDesktopApplicationState database sidecars", () => {
|
||||
|
||||
it("reports cancellation during the last database read instead of Evidence", async () => {
|
||||
const controller = new AbortController();
|
||||
const reason = { stage: "last database read" };
|
||||
let reads = 0;
|
||||
const folder = await writeCaptureFolder(
|
||||
extended({
|
||||
@@ -925,7 +926,7 @@ describe("inspectDesktopApplicationState database sidecars", () => {
|
||||
...readers,
|
||||
sqlite: async (database, signal) => {
|
||||
reads += 1;
|
||||
if (reads === 3) controller.abort(new Error("stopped"));
|
||||
if (reads === 3) controller.abort(reason);
|
||||
return readers.sqlite(database, signal);
|
||||
},
|
||||
},
|
||||
@@ -934,6 +935,9 @@ describe("inspectDesktopApplicationState database sidecars", () => {
|
||||
|
||||
expect(reads).toBe(3);
|
||||
expect(result).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(result instanceof AnalysisCancelledError && result.cause).toBe(
|
||||
reason,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -321,6 +321,7 @@ it.each([
|
||||
expect(result.error.cleanupResources).toContain(join(bundle, suffix));
|
||||
if (cancel) {
|
||||
expect(result.error._tag).toBe("AnalysisCancelledError");
|
||||
expect(result.error.cause).toBe(reason);
|
||||
expect(result.error.partialObservation).toMatchObject({
|
||||
kind: "partial-task-measurement",
|
||||
solves: [{}, {}, {}],
|
||||
|
||||
@@ -57,11 +57,14 @@ it("keeps the place of a call cancelled while it waits, so the one after it stil
|
||||
return ok("last");
|
||||
});
|
||||
await settled();
|
||||
caller.abort(new Error("the client went away"));
|
||||
expect(await cancelled).toMatchObject({
|
||||
const reason = { caller: "the client went away" };
|
||||
caller.abort(reason);
|
||||
const cancellation = await cancelled;
|
||||
expect(cancellation).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisCancelledError", operation: "cancelled" },
|
||||
});
|
||||
expect(!cancellation.ok && cancellation.error.cause).toBe(reason);
|
||||
await settled();
|
||||
expect(log).toEqual(["running"]);
|
||||
running.end();
|
||||
|
||||
@@ -81,9 +81,9 @@ it("waits for an interrupted reader before removing its folder", async () => {
|
||||
|
||||
it("preserves a completed view when cancellation arrives during successful cleanup", async () => {
|
||||
const controller = new AbortController();
|
||||
const reason = { stage: "reader final cleanup" };
|
||||
const owner = new DesktopRemoteTaskReaders(async (argv) => {
|
||||
if (argv[3] === PYTHON_READER_CLEANUP)
|
||||
controller.abort(new Error("cancelled"));
|
||||
if (argv[3] === PYTHON_READER_CLEANUP) controller.abort(reason);
|
||||
return success;
|
||||
});
|
||||
const failure = await owner
|
||||
@@ -91,6 +91,7 @@ it("preserves a completed view when cancellation arrives during successful clean
|
||||
.catch((error: unknown) => error);
|
||||
if (!(failure instanceof AnalysisCancelledError))
|
||||
throw new Error("expected cancellation");
|
||||
expect(failure.cause).toBe(reason);
|
||||
expect(failure.diagnostics).toMatchObject({
|
||||
completed_reader_views: { note: { view: { value: 7 } } },
|
||||
});
|
||||
@@ -99,12 +100,13 @@ it("preserves a completed view when cancellation arrives during successful clean
|
||||
|
||||
it("keeps cancellation and a completed view when final cleanup also fails", async () => {
|
||||
const controller = new AbortController();
|
||||
const reason = { stage: "reader final cleanup" };
|
||||
let fail = true;
|
||||
const owner = new DesktopRemoteTaskReaders(async (argv) => {
|
||||
if (argv[3] !== PYTHON_READER_CLEANUP) return success;
|
||||
if (fail) {
|
||||
fail = false;
|
||||
controller.abort(new Error("cancelled"));
|
||||
controller.abort(reason);
|
||||
return { ...success, exit_code: 1, stderr: "denied" };
|
||||
}
|
||||
return success;
|
||||
@@ -115,6 +117,7 @@ it("keeps cancellation and a completed view when final cleanup also fails", asyn
|
||||
if (!(failure instanceof ProviderCleanupError))
|
||||
throw new Error("expected cleanup failure");
|
||||
expect(failure.cleanupPrimary).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(failure.cleanupPrimary?.cause).toBe(reason);
|
||||
expect(failure.diagnostics).toMatchObject({
|
||||
completed_reader_views: { note: { view: { value: 7 } } },
|
||||
});
|
||||
|
||||
@@ -736,7 +736,9 @@ describe("buildEnvironment dry-run package tarball", () => {
|
||||
code: "invalid_request",
|
||||
details: { issues: [{ path: ["package_tarball"] }] },
|
||||
});
|
||||
expect(await readdir(root)).not.toContain("bundle");
|
||||
await expect(readdir(input.directory!)).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
} finally {
|
||||
read.mockRestore();
|
||||
}
|
||||
|
||||
Loaded 100 of 357 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user