chore: merge upstream main while preserving bug fixes

# Conflicts:
#	README.md
This commit is contained in:
morluto 2026-10-14 09:50:13 +08:00
commit e84c58dabf
65 changed files with 4850 additions and 57 deletions

View File

@ -122,7 +122,7 @@ explicit path or endpoint and do not need it.
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`.
- 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.

View File

@ -8,9 +8,11 @@ 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`. 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.
`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

View File

@ -64,6 +64,11 @@ Setup registers REA with your agent and installs matching workflow instructions.
## Quick start
On Linux, Ghidra, JADX, firmware tool launches and static JavaScript/Electron
analysis require a procps-compatible `ps` on REA's `PATH` for process ownership
checks. Recovery steps are in `docs/installation.md` under
"Linux owned-process prerequisite".
### Set up your agent
With Node.js and npm installed, run:
@ -156,8 +161,6 @@ CLI commands use the same workflows.
REA requires Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+, plus npm.
Additional tools and host support depend on the target:
Linux supervised analysis also requires a procps-compatible `ps` on REA's
`PATH`; distributions provide it through `procps` or `procps-ng`.
| Target | What REA returns | Requirements and guide |
| ---------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
@ -169,7 +172,7 @@ Linux supervised analysis also requires a procps-compatible `ps` on REA's
| EVM bytecode | Dispatch selectors, byte offsets, inferred arguments and mutability | Local raw/hex carrier on Linux x64; [offline EVM guide](docs/evm-bytecode.md) |
| SQLite databases | Tables, columns, indexes, views, triggers and selected ordinary-table rows | Local database/WAL snapshot; Node.js 24.x >=24.15 or 26+ with native SQLite limits |
| Recorded Linux crashes | Raw notes, every recorded thread's registers/signals and optional mapping candidates | Caller-supplied pwntools on Linux x64; optional GDB/pwndbg; [recorded crashes](docs/recorded-crashes.md) |
| JavaScript / Electron | Modules, imports, source maps, routes, IPC and native add-on relationships | Node.js and npm; [application analysis](docs/javascript-application-workflows.md) |
| JavaScript / Electron | Modules, imports, source maps, routes, IPC and native add-on relationships | Node.js and npm; Linux: procps-compatible `ps`; [application analysis](docs/javascript-application-workflows.md) |
| Websites | Page structure, scripts, network observations and requested screenshots | A Chrome-family browser; [browser analysis](docs/browser-observation.md) |
| Saved network captures | Requests, responses, exposed payloads and source locations | HAR or Fiddler Everywhere SAZ; mitmdump on Linux for native mitmproxy; [capture guide](docs/web-network-captures.md) |
| .NET assemblies | Metadata, CIL instructions, declared native dependencies and build comparisons | None (static only); [managed-code guide](docs/managed-code-analysis.md) |
@ -337,7 +340,7 @@ Report vulnerabilities through [SECURITY.md](SECURITY.md).
## Star history
🎉 **180,000 GitHub stars — thank you!**
🎉 **200,000 GitHub stars — thank you!**
Thanks to everyone using REA, reporting bugs, requesting features, testing builds, and contributing fixes.

View File

@ -44,6 +44,7 @@ export default defineConfig({
link: "/web-network-captures",
},
{ text: "Electron observation", link: "/electron-observation" },
{ text: "Electron fuses", link: "/electron-fuses" },
{ text: "Process capture", link: "/process-capture" },
{ text: ".NET assemblies", link: "/managed-code-analysis" },
{ text: "Android applications", link: "/android-analysis" },

View File

@ -9,6 +9,10 @@ This family is present on repository main and in npm 4.1.0. Check the
[released package boundary](installation.md#released-package-and-main) and the
connected server's tool list before selecting it.
On Linux, JADX launches an REA-owned process and requires a procps-compatible
`ps` on REA's `PATH`; see the shared
[Linux process prerequisite](installation.md#linux-owned-process-prerequisite).
## Optional package delivery
The MCP `inspect_android_package` request keeps complete output when `detail` is

View File

@ -292,6 +292,7 @@ Choose the guide for the target and intended interaction:
- [Browser observation](browser-observation.md): inspect a selected page through CDP.
- [Browser scenarios](browser-scenario-contract.md): run declared interactions and capture their results.
- [Electron observation](electron-observation.md): inspect an Electron renderer or capture an application scenario.
- [Electron fuses](electron-fuses.md): read packaged fuse bytes before expecting `--inspect` to reach an Electron main process.
- [Node/Electron Inspector](javascript-runtime-observation.md): record script locations and execution contexts.
- [Process capture](process-capture.md): run an executable and compare terminal, exit and filesystem observations.

82
docs/electron-fuses.md Normal file
View File

@ -0,0 +1,82 @@
# Electron fuse state
`inspect_electron_fuses` / `inspect-electron-fuses` answers whether a packaged
Electron app will honor `--inspect` and `--inspect-brk`, `ELECTRON_RUN_AS_NODE`,
embedded ASAR integrity validation, and loading application code only from
`app.asar`. It reads the fuse wire stored in the Electron binary. It does not
launch the app, modify fuse bytes, or use the network.
```bash
rea inspect-electron-fuses /Applications/Example.app --json
```
The CLI resolves a relative path against the caller's working directory. MCP
requires an absolute filesystem path and preserves that exact selection:
```json
{
"name": "inspect_electron_fuses",
"arguments": { "path": "/Applications/Example.app" }
}
```
Check this before `observe_javascript_runtime` with `electron-main`. When
`EnableNodeCliInspectArguments` is disabled or removed, Electron ignores
Inspector flags, and a later target listing can simply show no main-process
target.
## What is read
On macOS, pass the `.app` bundle or the Electron Framework binary. Bundle and
framework selection first tries the top-level `Electron Framework` binary,
then `Versions/Current/Electron Framework`, then
`Versions/A/Electron Framework`. Symlinks are followed only while they stay
inside that framework.
On Windows and Linux, pass the main Electron executable. The wire lives in
that executable rather than in `app.asar`.
The inline Evidence result contains:
- the selected path, the resolved regular file, its SHA-256, size, and container
format (`mach-o`, `mach-o-universal`, `pe`, or `elf`);
- one slice entry for a thin Mach-O, PE, or ELF file, and one entry per slice of
a universal Mach-O, with file-absolute `sentinel_offset` values;
- `wire_version`, `declared_length`, and ordered fuses `{ index, name, state, raw_byte }`;
- `sentinel_count`, including zero and more than one;
- `runtime_implications`, each marked `relationship: "derivation"` and tied to
the fuse index it came from.
Version 1 names, in index order, are `RunAsNode`, `EnableCookieEncryption`,
`EnableNodeOptionsEnvironmentVariable`, `EnableNodeCliInspectArguments`,
`EnableEmbeddedAsarIntegrityValidation`, `OnlyLoadAppFromAsar`,
`LoadBrowserProcessSpecificV8Snapshot`, `GrantFileProtocolExtraPrivileges`,
and `WasmTrapHandlers`. An index past that table, or any index on another wire
version, keeps `name: null` and the raw byte.
Wire bytes are `0x30` disabled, `0x31` enabled, and `0x72` removed. Any other
byte is `unknown`. A removed fuse is not enabled: Inspector flags and
`ELECTRON_RUN_AS_NODE` are not honored, and application code is not restricted
to `app.asar`. On macOS and Windows, a removed or disabled
`EnableEmbeddedAsarIntegrityValidation` byte means embedded ASAR integrity is
not enforced, and an enabled byte means it is enforced. Electron compiles that
validation only on macOS and Windows. On a Linux ELF executable the observed
byte is preserved and the enforcement outcome is `unknown`.
## Unknown and multiple sentinels
No sentinel means the fuse state is unknown. It does not mean the fuses are
disabled. A wire that continues past the slice is reported as truncated, with
only the fuse bytes that were present. Two or more sentinels in one slice are
all reported. REA does not pick one.
Fuse bytes do not prove that the application enables Inspector some other way,
for example through `app.commandLine` or a remote-debugging switch in
application code.
The selected file must be a regular file, or a bundle that resolves to one.
Symlinks are followed only inside `Electron Framework.framework`. The reader
admits at most 1 GiB, searches with 256 KiB windows, and keeps at most 32
sentinels per slice. A real nine-fuse, one-slice result is about 1,200 CLI
tokens; a two-slice result is about 1,300 tokens. Exact size varies with path
lengths and limitations. This inspection creates no temporary files and does
not modify the selected bytes.

View File

@ -7,6 +7,10 @@ This passive runtime surface is distinct from the target-free static
workflow. Static analysis reads a local ASAR or extracted directory without
executing application code. Passive observation inspects an existing Electron
page. The results keep static inferences and runtime observations distinct.
Packaged fuse state is a separate static read:
[`inspect_electron_fuses`](electron-fuses.md) reports whether this binary will
honor Inspector flags, `ELECTRON_RUN_AS_NODE`, embedded ASAR integrity, and
`app.asar`-only loading.
Use the separate
[`reconcile_javascript_runtime`](javascript-runtime-reconciliation.md) workflow
when both Evidence sets already exist.

View File

@ -9,6 +9,10 @@ No device, root privilege, mount, emulation or execution of extracted programs
is needed. Arbitrary firmware bytes do not pass native executable admission;
select a returned ELF separately for supported Ghidra analysis.
On Linux, Binwalk and Unblob run as REA-owned processes and require a
procps-compatible `ps` on REA's `PATH`; see the shared
[Linux process prerequisite](installation.md#linux-owned-process-prerequisite).
## Caller-supplied tools
Provide existing tools using absolute executable paths, scoped to your shell:

View File

@ -7,6 +7,8 @@ Executable bytes remain unchanged.
Use [installation](installation.md#ghidra) for supported hosts, prerequisites,
configuration, startup deadlines, resource controls, and cleanup recovery.
[Linux Ghidra launches](installation.md#linux-owned-process-prerequisite) also
require a procps-compatible `ps` on REA's `PATH` for process ownership checks.
[Windows P0](windows-ghidra-p0.md) has its own admission and lifecycle boundary;
[NativeAOT observations](ghidra-nativeaot.md) include a built-in read-only PE
snapshot parser and a separate optional annotation extension. Check the

View File

@ -52,7 +52,7 @@ For runtime instrumentation, see the [Frida guide](./frida-instrumentation.md).
| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) |
| Select a native provider or import format | [Ghidra semantics](ghidra-provider.md), [Windows](windows-ghidra-p0.md), [DOS](ghidra-dos.md), [raw bytes](raw-binary.md), [MIPS](ghidra-mips.md), [PSP](ghidra-psp.md), [NativeAOT](ghidra-nativeaot.md), [IDA](ida-provider.md), [JEB](jeb-analysis.md) |
| Analyze JavaScript or Electron artifacts | [Static analysis](javascript-artifact-reconstruction.md), [tracing and comparisons](javascript-application-workflows.md), [application graph](javascript-application-graph.md), [source recovery](javascript-recovery.md) |
| Compare static JavaScript with runtime observations | [Runtime attribution](javascript-runtime-observation.md), [reconciliation](javascript-runtime-reconciliation.md), [Electron observation](electron-observation.md) |
| Compare static JavaScript with runtime observations | [Runtime attribution](javascript-runtime-observation.md), [reconciliation](javascript-runtime-reconciliation.md), [Electron observation](electron-observation.md), [Electron fuses](electron-fuses.md) |
| Inspect assemblies, packages, or firmware | [.NET](managed-code-analysis.md), [Android](android-analysis.md), [Apktool resources](apktool-resource-analysis.md), [ADB acquisition](adb-device-analysis.md), [Apple](apple-application-analysis.md), [Harmony](harmony-analysis.md), [firmware](firmware-analysis.md), [WASM](wasm-artifacts.md), [EVM](evm-bytecode.md) |
| Observe or compare a browser scenario | [CDP observation](browser-observation.md), [scenarios](browser-scenario-contract.md), [runtime evidence](web-runtime.md), [historical captures](web-network-captures.md) |
| Trace captured website source | [Source locations](web-source-location.md), [module traces](website-module-trace.md), [script export](website-script-export.md) |

View File

@ -28,11 +28,6 @@ For the latest published release, use `npx rea-agents@latest setup`.
[Package-runner and global-installation options](#package-runners-and-global-installation)
cover explicit versions, Bun, runtime requirements, and the curl wrapper.
On Linux, supervised analysis requires a procps-compatible `ps` on REA's
`PATH` so REA can inspect process ownership before launching a child. Install
the `procps` or `procps-ng` package provided by your distribution. This
prerequisite also applies to JavaScript and Electron workflows.
## Review setup changes
`rea setup` first offers the supported agents in a multi-select. Existing REA
@ -325,6 +320,21 @@ on `open_binary`. A `capability_unavailable` failure carries
`details.candidate_ids`, while `provider_unavailable` asks you to repair the
selected engine. See [provider selection](cli.md#choose-a-provider).
### Linux owned-process prerequisite
On Linux, REA's shared process supervisor requires a procps-compatible `ps`
on REA's `PATH` for ownership checks before launching a child. Ghidra, JADX,
firmware tools and static JavaScript/Electron analysis use these checks.
Install your distribution's `procps` or `procps-ng` package, or put another
procps-compatible `ps` on `PATH` before
running those workflows. A missing or incompatible command causes a refusal
before the supervised child starts.
The public error code varies by caller: `capability_unavailable`,
`provider_unavailable` or `execution_failure`. Some provider adapters report
only a generic startup error. Check this prerequisite before following
provider-specific recovery advice.
## Diagnose, update, and remove
`rea doctor --json` is strictly read-only. `rea update` updates only the npm or Bun global installation that owns the running CLI. Source checkouts and package-runner copies must be updated through the mechanism that owns them; a fresh package-runner invocation can use `npx rea-agents@latest` or `bunx rea-agents@latest`. `rea uninstall` removes only REA-owned agent registrations and skill files; `--purge-data` additionally removes REA cache and state paths.

View File

@ -36,6 +36,12 @@ application starts, and REA never resumes it or sends
`Runtime.runIfWaitingForDebugger`. Resume a paused target with a separate
caller-owned debugger before requesting application runtime observations.
For a packaged Electron app, read
[`inspect_electron_fuses`](electron-fuses.md) before starting an `electron-main`
target. When `EnableNodeCliInspectArguments` is disabled or removed, Electron
ignores `--inspect` and `--inspect-brk`. A missing main-process target after
launch does not, by itself, show that fuse.
The caller supplies the literal-loopback HTTP endpoint directly. Selecting an endpoint exposes all targets it serves and their local script locations or renderer origins. A target must match the exact requested target ID and expose a same-port WebSocket reported by that endpoint. Targets explicitly marked attached are rejected rather than displacing another debugger. Node's `/json/list` omits attachment state, so REA preserves it as `null` (unknown), reports that limitation, and permits observation unless the endpoint explicitly reports `attached: true`. Local paths are canonicalized after symlink resolution.
The `attached` field in JavaScript runtime and browser target results is nullable: `true` and `false` are reported only when discovery supplied a boolean; `null` means the endpoint omitted or could not report the state. CDP `TargetInfo` worker records require an `attached` boolean; REA excludes a worker record with a missing or malformed value as an invalid protocol value.

View File

@ -270,7 +270,7 @@ npx rea-agents@latest setup
## سجل النجوم
🎉 **170,000 نجمة على GitHub — شكرًا لكم!**
🎉 **200,000 نجمة على GitHub — شكرًا لكم!**
شكرًا لكل من يستخدم REA، ويبلّغ عن الأخطاء، ويقترح ميزات، ويختبر إصدارات البناء، ويساهم بالإصلاحات.

View File

@ -268,7 +268,7 @@ Melde Sicherheitslücken gemäß [SECURITY.md](../../SECURITY.md).
## Sternverlauf
🎉 **170.000 GitHub-Sterne – vielen Dank!**
🎉 **200.000 GitHub-Sterne – vielen Dank!**
Danke an alle, die REA nutzen, Fehler melden, Funktionswünsche äußern, Builds testen und Fehlerbehebungen beitragen.

View File

@ -281,7 +281,7 @@ npx rea-agents@latest setup
## Ιστορικό αστεριών
🎉 **180.000 αστέρια στο GitHub — σας ευχαριστούμε!**
🎉 **200.000 αστέρια στο GitHub — σας ευχαριστούμε!**
Ευχαριστούμε όλους όσοι χρησιμοποιούν το REA, αναφέρουν σφάλματα, ζητούν νέες λειτουργίες, δοκιμάζουν builds και συνεισφέρουν διορθώσεις.

View File

@ -268,7 +268,7 @@ Informa de vulnerabilidades siguiendo [SECURITY.md](../../SECURITY.md).
## Historial de estrellas
🎉 **170.000 estrellas en GitHub: ¡gracias!**
🎉 **200.000 estrellas en GitHub: ¡gracias!**
Gracias a quienes usan REA, informan de errores, solicitan funciones, prueban compilaciones y contribuyen con correcciones.

View File

@ -253,7 +253,7 @@ npx rea-agents@latest setup
## تاریخچهٔ ستاره‌ها
🎉 **۱۷۰٬۰۰۰ ستارهٔ GitHub — سپاسگزاریم!**
🎉 **۲۰۰٬۰۰۰ ستارهٔ GitHub — سپاسگزاریم!**
از همهٔ کسانی که از REA استفاده می‌کنند، خطاها را گزارش می‌دهند، قابلیت‌های جدید پیشنهاد می‌کنند، buildها را آزمایش می‌کنند و در بهبود پروژه مشارکت دارند سپاسگزاریم.

View File

@ -272,7 +272,7 @@ Signalez les vulnérabilités via [SECURITY.md](../../SECURITY.md).
## Historique des étoiles
🎉 **170 000 étoiles GitHub — merci !**
🎉 **200 000 étoiles GitHub — merci !**
Merci à toutes les personnes qui utilisent REA, signalent des bugs, proposent des fonctionnalités, testent les builds et contribuent des correctifs.

View File

@ -293,7 +293,7 @@ Laporkan kerentanan melalui [SECURITY.md](../../SECURITY.md).
## Riwayat Bintang
🎉 **170.000 bintang GitHub, terima kasih!**
🎉 **200.000 bintang GitHub, terima kasih!**
Terima kasih kepada semua orang yang menggunakan REA, melaporkan bug, meminta fitur, menguji build, dan berkontribusi dalam perbaikan.

View File

@ -272,7 +272,7 @@ Segnala vulnerabilità tramite [SECURITY.md](../../SECURITY.md).
## Cronologia delle stelle
🎉 **170.000 stelle su GitHub — grazie!**
🎉 **200.000 stelle su GitHub — grazie!**
Grazie a tutti coloro che usano REA, segnalano bug, propongono funzionalità, testano le build e contribuiscono con correzioni.

View File

@ -270,7 +270,7 @@ npx rea-agents@latest setup
## スター履歴
🎉 **GitHub スター 170,000 件、ありがとうございます!**
🎉 **GitHub スター 200,000 件、ありがとうございます!**
REA の利用、バグ報告、機能の要望、ビルドのテスト、修正への貢献に感謝します。

View File

@ -277,7 +277,7 @@ npx rea-agents@latest setup
## 스타 기록
🎉 **GitHub 스타 170,000개, 감사합니다!**
🎉 **GitHub 스타 200,000개, 감사합니다!**
REA를 사용하고, 버그를 보고하고, 기능을 요청하고, 빌드를 테스트하고, 수정에 기여해 주신 모든 분께 감사드립니다.

View File

@ -268,7 +268,7 @@ Zgłaszaj podatności zgodnie z [SECURITY.md](../../SECURITY.md).
## Historia gwiazdek
🎉 **170 000 gwiazdek na GitHubie — dziękujemy!**
🎉 **200 000 gwiazdek na GitHubie — dziękujemy!**
Dziękujemy wszystkim, którzy używają REA, zgłaszają błędy, proponują nowe funkcje, testują kompilacje i przesyłają poprawki.

View File

@ -268,7 +268,7 @@ Relate vulnerabilidades conforme [SECURITY.md](../../SECURITY.md).
## Histórico de estrelas
🎉 **170.000 estrelas no GitHub — muito obrigado!**
🎉 **200.000 estrelas no GitHub — muito obrigado!**
Obrigado a todos que usam o REA, relatam bugs, sugerem funcionalidades, testam builds e contribuem com correções.

View File

@ -268,7 +268,7 @@ npx rea-agents@latest setup
## История звёзд
🎉 **170 000 звёзд на GitHub — спасибо!**
🎉 **200 000 звёзд на GitHub — спасибо!**
Спасибо всем, кто использует REA, сообщает об ошибках, предлагает новые функции, тестирует сборки и помогает с исправлениями.

View File

@ -268,7 +268,7 @@ npx rea-agents@latest setup
## ประวัติดาว
🎉 **170,000 ดาวบน GitHub — ขอบคุณทุกคน!**
🎉 **200,000 ดาวบน GitHub — ขอบคุณทุกคน!**
ขอบคุณทุกคนที่ใช้ REA รายงานบั๊ก เสนอฟีเจอร์ ทดสอบบิลด์ และช่วยแก้ไข

View File

@ -273,7 +273,7 @@ Güvenlik açıklarını [SECURITY.md](../../SECURITY.md) üzerinden bildirin.
## Yıldız geçmişi
🎉 **GitHub'da 170.000 yıldız — teşekkürler!**
🎉 **GitHub'da 200.000 yıldız — teşekkürler!**
REA'yı kullanan, hata bildiren, özellik talep eden, derlemeleri test eden ve düzeltmelere katkıda bulunan herkese teşekkürler.

View File

@ -268,7 +268,7 @@ npx rea-agents@latest setup
## Історія зірок
🎉 **170 000 зірок на GitHub — дякуємо!**
🎉 **200 000 зірок на GitHub — дякуємо!**
Дякуємо всім, хто використовує REA, повідомляє про помилки, пропонує нові функції, тестує збірки й допомагає з виправленнями.

View File

@ -268,7 +268,7 @@ Báo cáo lỗ hổng theo [SECURITY.md](../../SECURITY.md).
## Lịch sử sao
🎉 **170.000 sao trên GitHub — xin cảm ơn!**
🎉 **200.000 sao trên GitHub — xin cảm ơn!**
Cảm ơn mọi người đã sử dụng REA, báo lỗi, đề xuất tính năng, kiểm thử bản dựng và đóng góp bản sửa lỗi.

View File

@ -274,7 +274,7 @@ npx rea-agents@latest setup
## Star 歷史
🎉 **GitHub Star 達到 170,000 個,感謝大家!**
🎉 **GitHub Star 達到 200,000 個,感謝大家!**
感謝每一位使用 REA、報告錯誤、提出功能需求、測試建置和貢獻修復的朋友。

View File

@ -274,7 +274,7 @@ npx rea-agents@latest setup
## Star 历史
🎉 **GitHub Star 达到 170,000 个,感谢大家!**
🎉 **GitHub Star 达到 200,000 个,感谢大家!**
感谢每一位使用 REA、报告错误、提出功能需求、测试构建和贡献修复的朋友。

View File

@ -19,6 +19,10 @@ Follow-up: [SQLite existing-control measurements](sqlite-existing-controls.md)
tests QGIS's shipped GeoPackage through the existing column selection, preserving
full selected-table schema and typed cells before considering another API.
Follow-up: [small real PCAP recordings](pcap-small-captures.md) compares existing
complete delivery with summary/pages. Preserve the negative SDK/model input
results: compact captures do not justify another production mechanism.
Follow-up: [native PE/DOS entry views and real model verification](native-entry-views.md)
records the post-merge native continuation, including a small-function case where
four selected replies cost more bytes than the complete dossier.

View File

@ -180,7 +180,7 @@ def main():
'inspect_android_package', 'search_android_classes',
'inspect_android_class', 'inspect_android_method',
'inspect_firmware_regions', 'inspect_managed_artifact',
'inspect_sqlite_database',
'inspect_sqlite_database', 'inspect_pcap_capture',
'inspect_managed_members', 'inspect_managed_native_boundaries',
'inspect_artifact', 'inspect_wasm_artifact',
'inspect_evm_interface', 'get_evidence_bundle', 'export_evidence_bundle',

View File

@ -70,6 +70,44 @@ for (const [side, answer] of [
value: answer.evidence_ids,
});
}
// PCAP delivery observations preserve each actual receipt's identity, limitations
// and view coverage. Summary/page scopes differ from complete source scope;
// retain them separately rather than equating page exhaustion with capture
// coverage. Repeated equivalent deliveries must also match every observation
// field except the receipt and parent IDs, which identify their own invocations.
if (
["wireshark-http.cap", "wireshark-http_redirects.pcapng"].includes(
left.artifact,
)
) {
for (const [side, answer] of [
["left", a],
["right", b],
]) {
if (
!Array.isArray(answer.delivery_observations) ||
!answer.delivery_observations.length
)
throw new Error("Missing original PCAP delivery observations");
runSpecific.push({
side,
path: ["delivery_observations"],
value: answer.delivery_observations,
});
if (left.delivery === right.delivery) {
answer.delivery_observations = answer.delivery_observations.map(
(observation) =>
Object.fromEntries(
Object.entries(observation).filter(
([key]) => key !== "evidence_id" && key !== "parent_evidence_id",
),
),
);
} else {
delete answer.delivery_observations;
}
}
}
// Binwalk assigns a new opaque region UUID in each invocation. Preserve both
// original lookup values in this report; compare the observed region facts
// separately. The product and original replies retain those IDs unchanged.

View File

@ -263,6 +263,34 @@
"url": "https://raw.githubusercontent.com/qgis/QGIS/e1223b8e9e71fc50ee995ed556f711c54ccc101b/resources/data/world_map.gpkg",
"derivation": "unmodified official resource file; streaming Git blob and SHA-256 verification"
}
},
{
"id": "wireshark-http.cap",
"file": "http.cap",
"bytes": 25803,
"sha256": "25a72bdf10339f2c29916920c8b9501d294923108de8f29b19aba7cc001ab60d",
"provenance": {
"project": "Wireshark public sample captures",
"source_page": "https://wiki.wireshark.org/SampleCaptures",
"url": "https://wiki.wireshark.org/uploads/27707187aeb30df68e70c8fb9d614981/http.cap",
"resolved_url": "https://gitlab.com/wireshark/wireshark/-/wikis/uploads/27707187aeb30df68e70c8fb9d614981/http.cap",
"upstream_expected_digest": null,
"derivation": "Unmodified capture published by official Wireshark wiki. Local digest pins downloaded bytes; no independently published upstream digest is claimed."
}
},
{
"id": "wireshark-http_redirects.pcapng",
"file": "http_redirects.pcapng",
"bytes": 47804,
"sha256": "ff432a6b71d06cd20720f7763e584014edd0a43c3b55981c682d1b7640207abc",
"provenance": {
"project": "Wireshark public sample captures",
"source_page": "https://wiki.wireshark.org/SampleCaptures",
"url": "https://wiki.wireshark.org/uploads/__moin_import__/attachments/SampleCaptures/http_redirects.pcapng",
"resolved_url": "https://gitlab.com/wireshark/wireshark/-/wikis/uploads/__moin_import__/attachments/SampleCaptures/http_redirects.pcapng",
"upstream_expected_digest": null,
"derivation": "Unmodified capture published by official Wireshark wiki. Local digest pins downloaded bytes; no independently published upstream digest is claimed."
}
}
]
}

View File

@ -523,7 +523,207 @@ const inspectEvmTarget = async () => {
};
};
const PCAP_EVIDENCE_ID = /^ev_[a-f0-9]{64}$/u;
const pcapObservation = (evidence, requestedView, parseEvidence) => {
// Authenticate with the selected runtime's owner, preserving the original
// envelope for measurement and provenance rather than reconstructing it.
parseEvidence(evidence);
const evidenceId = evidence.evidence_id;
const value = normalized(evidence);
for (const key of ["limitations", "unknowns"])
if (
Object.hasOwn(value, key) &&
(!Array.isArray(value[key]) ||
!value[key].every((item) => typeof item === "string"))
)
throw new Error("PCAP normalized provenance lacks a valid string list");
const target =
value.kind === "summary"
? value.summary?.target
: value.kind === "pcap"
? value.artifact
: value.target;
if (
evidence.subject?.digest?.sha256 !== inputSha ||
evidence.subject?.local_path !== values.path ||
target?.sha256 !== inputSha ||
target?.path !== values.path ||
(value.kind === "summary" &&
(value.artifact?.sha256 !== inputSha ||
value.artifact?.path !== values.path))
)
throw new Error("PCAP Evidence does not identify the selected capture");
// Summary delivery delegates to a retained view, whose Evidence operation is
// inspect_analysis_view even when inspect_pcap_capture was requested.
const operation = requestedView
? "inspect_analysis_view"
: "inspect_pcap_capture";
if (evidence.operation !== operation)
throw new Error(
"PCAP Evidence operation differs from the requested delivery",
);
if (
requestedView &&
(typeof value.parent_evidence_id !== "string" ||
!PCAP_EVIDENCE_ID.test(value.parent_evidence_id) ||
value.parent_evidence_id === evidenceId ||
!Array.isArray(evidence.evidence_links) ||
!evidence.evidence_links.includes(value.parent_evidence_id))
)
throw new Error("PCAP retained parent lacks a distinct Evidence link");
if (
requestedView &&
value.parent_digest !== value.parent_evidence_id.slice(3)
)
throw new Error("PCAP retained parent digest differs from its identity");
const parameters = requestedView
? { parent_evidence_id: value.parent_evidence_id, view: requestedView }
: { path: values.path };
if (
!isDeepStrictEqual(evidence.parameters, parameters) ||
(requestedView && !isDeepStrictEqual(value.view, requestedView))
)
throw new Error("PCAP Evidence parameters or view differ from the request");
return {
evidence_id: evidenceId,
limitations: evidence.limitations,
...Object.fromEntries(
[
"kind",
"view",
"parent_evidence_id",
"coverage",
"limitations",
"unknowns",
]
.filter((key) => Object.hasOwn(value, key) && key !== "limitations")
.map((key) => [key, value[key]]),
),
...(Object.hasOwn(value, "limitations")
? { normalized_limitations: value.limitations }
: {}),
};
};
const inspectPcapTarget = async () => {
const { parseEvidence } = await import(
pathToFileURL(resolve(values["runtime-root"], "dist/domain/evidence.js"))
.href
);
const input = {
path: values.path,
...(values.delivery === "summary" ? { detail: "summary" } : {}),
};
const definition = catalogDefinitions.get("inspect_pcap_capture");
const ajv = new Ajv2020({ strict: false, validateFormats: false });
if (
!definition ||
!ajv.validateSchema(definition.inputSchema) ||
!ajv.validate(definition.inputSchema, input)
)
throw new Error("Selected PCAP request is not advertised by this runtime");
const inspected = await call("inspect_pcap_capture", input);
const n = normalized(inspected);
const observations = [
pcapObservation(
inspected,
values.delivery === "summary" ? { kind: "summary" } : undefined,
parseEvidence,
),
];
let capture, uris;
if (values.delivery === "summary") {
if (n.kind !== "summary" || n.parent_operation !== "inspect_pcap_capture")
throw new Error("Runtime returned no requested PCAP summary");
if (
!isDeepStrictEqual(n.coverage, {
status: "complete-within-view",
examined: 1,
total: 1,
next_offset: null,
exhausted: true,
})
)
throw new Error("PCAP summary coverage differs from its complete view");
capture = n.summary;
const parentEvidenceId = observations[0].parent_evidence_id;
const view = { kind: "pcap", facet: "http-uris", offset: 0, limit: 2 };
const selected = await call("inspect_analysis_view", {
source: { kind: "retained-evidence", evidence_id: parentEvidenceId },
view,
});
const page = normalized(selected);
const observation = pcapObservation(selected, view, parseEvidence);
const total = capture.retained_counts.http_request_uris;
const examined = Math.min(view.limit, total);
const expectedCoverage = {
status: "page",
examined,
total,
next_offset: examined < total ? examined : null,
exhausted: examined >= total,
};
if (
page.parent_evidence_id !== parentEvidenceId ||
page.parent_operation !== "inspect_pcap_capture" ||
page.kind !== "pcap" ||
page.page.facet !== "http-uris" ||
page.source_coverage !== capture.coverage ||
!isDeepStrictEqual(page.coverage, expectedCoverage)
)
throw new Error(
"PCAP page identity, scope or coverage differs from its parent",
);
uris = page.page.items;
if (
observations.some((item) => item.evidence_id === observation.evidence_id)
)
throw new Error("PCAP retained read reused an Evidence identity");
observations.push(observation);
} else {
const {
endpoints,
dns_queries,
tls_sni_values,
protocol_distribution,
http_request_uris,
unattributed_dns_addresses,
diagnostics,
...metadata
} = n;
capture = {
...metadata,
retained_counts: {
endpoints: endpoints.length,
dns_queries: dns_queries.length,
tls_sni_values: tls_sni_values.length,
protocol_distribution: protocol_distribution.length,
http_request_uris: http_request_uris.length,
unattributed_dns_addresses: unattributed_dns_addresses.length,
},
diagnostics_code_units:
diagnostics === undefined
? null
: {
tshark: diagnostics.tshark_stderr.length,
capinfos: diagnostics.capinfos_stderr.length,
},
};
uris = http_request_uris.slice(0, 2);
}
if (uris.length !== Math.min(2, capture.retained_counts.http_request_uris))
throw new Error("Runtime did not return the requested retained URI prefix");
return {
artifact_sha256: targetSha(inspected),
capture,
http_request_uris_first2: uris,
delivery_observations: observations,
evidence_ids: observations.map((item) => item.evidence_id),
};
};
const workflows = {
"wireshark-http.cap": inspectPcapTarget,
"wireshark-http_redirects.pcapng": inspectPcapTarget,
"qgis-world-map.gpkg": async () => {
const columns = ["fid", "NAME", "ISO_A3"];
const input = {

View File

@ -0,0 +1,291 @@
# Small real PCAP recordings: keep complete delivery
Continues [#2016](https://github.com/morluto/rea/issues/2016) after the QGIS
SQLite round. Measured main is pinned to
`3ae826ceba6f25b08bce50bcb51001209bc16b57`, with 23 intervening commits.
These include bounded PCAP summaries/pages, authenticated artifact subjects,
unknown-snapshot reuse and shared transcript grading. This round measures existing
PCAP delivery choices; it adds no production, API, default, cache or skill change.
## Corpus and question
Use two unmodified recordings published on the official
[Wireshark sample-capture page](https://wiki.wireshark.org/SampleCaptures):
| Recording | Bytes | SHA-256 |
| --------------------------------------- | -----: | ------------------------------------------------------------------ |
| HTTP request/response, `http.cap` | 25,803 | `25a72bdf10339f2c29916920c8b9501d294923108de8f29b19aba7cc001ab60d` |
| HTTP redirects, `http_redirects.pcapng` | 47,804 | `ff432a6b71d06cd20720f7763e584014edd0a43c3b55981c682d1b7640207abc` |
`corpus.json` preserves original/resolved URLs. Local digests pin downloaded
bytes; no independently published upstream digest or immutable source commit is
claimed. These are upstream saved application-protocol recordings, not generated
REA fixtures. Recorded clients and current deployments are unknown. No new
traffic is captured and no recorded target/URL is executed or fetched.
The question asks for complete returned identity/client/scalar metadata, every
observed count and source coverage, six retained inventory counts, explicit nulls,
diagnostic UTF-16 lengths, and the first two retained HTTP URIs in original order.
Preserve each used receipt's limitations and identity. Summary/page scope and
unknowns remain distinct from source coverage; exhaustion does not establish
network completeness. Other inventory lists and diagnostic text are unrequested.
Compare default `{path}` with existing `{path, detail: "summary"}` plus a retained
`http-uris` page, offset 0, limit 2. Ordinary selected-field
`inspect_packet_capture` remains a separate boundary, not a substitute claim
about complete inventories. `questions/pcap-http.txt` contains the fixed question.
## SDK measurements
Frozen dist/bridge file-table SHA-256:
`db050545e94dc305a48042e06df96ac6c64b21ee6c5c4707c787d5e46df79e47`.
Linux x64, Node 24.18.0, SDK client/server 2.3.1, installed Wireshark 4.0.17.
Explicit commands are `/usr/bin/tshark`, `/usr/bin/capinfos`, `/usr/bin/prlimit`;
private receipts retain executable hashes. Each workflow starts a fresh server
and uses actual named advertised definitions/schema validation. Host page caches
are not flushed. Both tools read the owned snapshot.
Eight trials run complete/summary/summary/complete per recording. Both pairs
match selected capture facts, URI prefixes, settings and original source bytes.
Each delivery's exact limitations, identities and coverage remain separately
recorded, rather than equating source coverage with page exhaustion.
| Trial | Reply bytes | SDK seconds | Payload CPU seconds | Kernel peak bytes |
| -------------------- | ----------: | ----------: | ------------------: | ----------------: |
| HTTP complete-1 | 7,601 | 4.349 | 5.336 | 611,778,560 |
| HTTP summary-1 | 17,540 | 4.972 | 5.973 | 601,661,440 |
| HTTP summary-2 | 17,540 | 5.101 | 6.092 | 611,438,592 |
| HTTP complete-2 | 7,601 | 4.463 | 5.503 | 617,148,416 |
| Redirects complete-1 | 7,535 | 4.670 | 5.641 | 610,627,584 |
| Redirects summary-1 | 17,240 | 5.099 | 6.104 | 615,522,304 |
| Redirects summary-2 | 17,240 | 5.060 | 6.056 | 610,385,920 |
| Redirects complete-2 | 7,535 | 4.466 | 5.458 | 608,141,312 |
Bytes are compact client-result JSON summed over one complete or two summary/page
calls, not model tokens or raw wire bytes. Summary/page expands replies to more
than twice the complete result on both recordings. All four SDK pairs are slower
and use more payload CPU; memory differences are small/inconsistent. CPU includes
driver/catalog work outside the SDK workflow timer.
Reproduce with `measure.mjs --artifact wireshark-http.cap` or
`wireshark-http_redirects.pcapng`, pinned path/runtime/revision, explicit provider
JSON, `--schemas selected`, and `--delivery complete|summary`. Use `compare.mjs`
on completed output directories. Original replies, catalogs, comparison receipts
and exact measured driver source stay private. Repeated deliveries in the same
mode also compare the ordered receipt observations, including limitations, view
parameters, coverage and unknowns; only receipt/parent IDs are invocation-specific.
Comparator regressions run with `npm run test:local -- tests/evaluation/analysisEfficiencyComparison.test.ts`
using synthetic measurement directories; they do not establish provider behavior.
## Actual model tasks
Four actual Codex CLI 0.161.0 tasks use requested `gpt-5.6-terra` high, the same
redirects question/runtime/artifact/provider settings, direct mode, two exact
enabled tools and equal context/discovery capture flags. The requested alias is
not verified server-resolved model identity. Equal cohort instructions select
complete or summary plus one URI page. The experiment driver applies its existing
private per-tool approval policy to `inspect_pcap_capture`; global configuration
and REA authority are unchanged.
| Trial | Input | Cached | Uncached | Output | CLI wall seconds | MCP calls | Facts |
| ---------- | -----: | -----: | -------: | -----: | ---------------: | --------: | ----- |
| complete-1 | 38,947 | 9,984 | 28,963 | 2,119 | 69.469 | 1 | pass |
| summary-1 | 58,779 | 38,144 | 20,635 | 3,241 | 141.304 | 2 | pass |
| summary-2 | 58,584 | 38,144 | 20,440 | 4,261 | 50.916 | 2 | pass |
| complete-2 | 38,917 | 23,040 | 15,877 | 2,813 | 106.896 | 1 | pass |
Summary/page total input **increases 50.92% / 50.54%** and output increases
52.95% / 51.48%. Uncached input is mixed: 28.75% lower / 28.74% higher. Time is
mixed: 103.41% slower / 52.37% faster. No cause is inferred for caching or timing
variation. Cached input is included in total; each CLI reports one final aggregate
usage. Output includes 1,149 / 1,162 / 2,196 / 1,837 reasoning tokens.
All calls succeed without extras/retries. Events contain only the selected MCP
tools and agent messages. All source-digest and private-profile cleanup checks
pass. Model payload CPU seconds/kernel peaks: 7.818/791,060,480;
9.793/752,705,536; 7.685/757,891,072; 7.999/750,993,408. These do not establish
a general CPU/memory gain.
The first summary initially fails the grader because it uses explicit equivalent
labels and a URI object retaining page/source coverage. Its facts are correct.
The grading boundary accepts these known forms while checking exact types,
null presence, original limitations and extra scopes against actual receipts.
Conflicting duplicates and unsupported claims remain failures. Each cited
receipt's own ID must be reported; its parent lookup ID is checked separately.
Original answers/reviews/grader source remain intact. No model is rerun. A grader
review pause separates the pairs; it is not task time or a cold-cache claim.
## Verification and decision
An independent stdlib raw PCAP/PCAPNG/Ethernet/IP/TCP/complete-HTTP-header parser
agrees on 43/271 packets, per-packet endpoint counts, first URI prefixes and 2/23
distinct HTTP URIs. It invokes no Wireshark. DNS associations, protocol-stack
dissection, nullable timestamps and live-network semantics are not independently
validated.
Both real CLI complete results match full MCP normalized results, subjects,
parameters, IDs and limitations. Portable CLI summary/URI views match MCP retained
normalized facts and subjects; source forms/caller parameters remain distinct.
Source bytes stay unchanged. Thirty-four context/grading regressions pass, including
cited complete metadata/counts/UTF-16 diagnostics and exact ordered response ID
lists that reject parent lookups, duplicates and nested representations. URI
prefix coverage must come from its cited page, even if an uncited page has the
same items. The SDK driver also checks the returned view against the exact
requested kind, facet, offset and limit. Five real-answer
mutations—omitted null, false page exhaustion, contradictory source scope,
unsupported absence claim and non-string receipt identity—fail correctly.
Malformed identities produce failed facts rather than crashing the grader.
JS syntax, Python compilation and
relevant standard lint pass.
A fresh real SDK summary/URI smoke matches the original selected facts. Three
private consumer fault injections alter the returned view's offset, limit or
presence after a real SDK reply; the driver rejects each. These establish the
experiment consumer's guard, not provider malformed-reply behavior. The initial
injection setup loaded the separate CommonJS client and did not inject a fault;
that discarded setup attempt remains private alongside the corrected ESM runs.
Original measured driver sources, token totals and timing data remain unchanged.
The driver also requires a distinct Evidence receipt for the retained URI page.
A private SDK consumer injection reuses the summary's response ID after a real
MCP reply: the preceding driver accepts it, the corrected driver rejects it
without writing an answer, and an unmodified real reply still passes. This
checks receipt uniqueness before recording observations; it does not establish
that a provider actually returned duplicate IDs.
The page guard checks the complete expected coverage object, including status,
continuation and exhaustion. Private actual-reply injections demonstrate that
the preceding driver accepts inconsistent coverage, while the corrected driver
rejects wrong status, next offset or exhaustion. Unmodified exhausted and
continuable URI pages still pass. The grader also requires each cited receipt's
subject digest and local path to identify the reference capture, and uses the
reference page's verified coverage. Thirty-four grading/context regressions pass,
including missing or malformed subjects and faithful but inconsistent coverage;
all four original model answers still pass factual regrading. Strict resource
grades remain false where terminal whole-scope counters are unknown.
PCAP observation production now validates both the Evidence subject and
normalized target against the selected path/digest for complete, summary and
page replies. Retained parents must be declared in each view receipt's Evidence
links and differ from that response's own ID. The grader enforces the same link
relationship. Private actual-reply injections and normal complete/summary
controls exercise this boundary; these do not establish a provider defect.
The grader preserves each receipt's producing tool while indexing it; conflicting
tool/envelope ownership for one ID remains ambiguous, while identical repeated
receipts are allowed. Complete delivery requires the capture tool/operation.
Summary delivery uses the capture tool but correctly delegates its Evidence
operation to `inspect_analysis_view`; the URI page uses that view tool/operation.
Both the grader and SDK observation producer validate this actual contract.
Wrong-tool/operation regressions fail before the correction and pass afterward;
private SDK operation faults are rejected while real complete/summary controls
and all four stored model factual grades still pass. No operation is rewritten
to make the tool name and delegated Evidence operation identical.
The receipt index parses the required envelope/nested-view containers before
allowing lookup, so missing or malformed normalized results/limitations become
failed facts with a written report. Empty observation lists also remain failed
facts and never enter citation checks that require a first receipt. Coverage uses type-sensitive JSON comparison
to distinguish booleans, integers and floats. Receipt-identity conflict checks
use the same type-sensitive rule, so a scalar type change cannot hide in a
repeated ID. Existing PCAP scope-alias checks already preserve JSON types.
Requested and cited URI views use the same type-sensitive JSON rule, rejecting
boolean or floating-point offsets/limits even when an answer faithfully copies
them. The observation producer rejects missing, null or malformed response IDs
before recording either capture or page receipts. Private real-reply injections
exercise these ID failures alongside normal complete/summary controls; they do
not establish malformed provider behavior.
Completed-call arguments stay with their producing tool and receipt in the
grader's identity index. Cited envelope parameters and actual call arguments
must match the selected path or retained parent/view, including delegated
summary parameters. The observation producer validates both summary and URI
views and envelope parameters at their common boundary. Private real-reply
faults reproduce missing-parameter/summary-view acceptance before the fix and
are rejected afterward; normal complete/summary controls still pass.
Negative regressions reproduce
prior crashes and false passes; all original real-model factual grades still
pass without model reruns. The SDK workflow is unchanged by these grader fixes.
**Decision: keep existing complete defaults; add no production optimization.**
For these compact recordings and this question, summary/page costs more total
input in both real-model pairs. No stable speed or CPU/memory benefit justifies
another API, automatic delivery switch, cache or plugin layer. Existing pages
remain available; their benefit needs evidence for the actual question/inventory.
Large captures, other protocols, Windows/macOS/Bun Wireshark and other model
workloads remain unverified here.
Heavy work stays serial with one CPU, 3 GiB early stop/4 GiB hard memory and no
swap. Build kernel peak: 2,575,732,736 bytes. Payload cleanup and zero phase limit
events are verified; missing terminal whole-scope counters remain unknown, so
strict whole-resource grades stay false. No raw capture, trace, credential,
runtime or result JSON is committed. No unrelated REA product bug is identified
or fixed, and no release is performed.
Review follow-up binds the cited URI page to its exact requested facet/range,
reported values and the summary retained parent. Small synthetic trial receipts
exercise the entire grading entrypoint, including faithful but wrong-page/range/
parent citations, missing nulls, false scope and malformed identities. These are
grader regressions, not real-provider evidence. All four stored actual model
answers still pass; no model reruns are required. Transcript readers close their
owned file handles after grading.
The SDK driver and factual grader require summary coverage to describe its one
complete view and require each present normalized limitations/unknowns field to
be a string array. Missing fields and valid empty arrays remain distinct.
Controlled consumer mutations over actual SDK replies reproduce the previously
accepted inconsistent summary coverage and verify rejection after the guard;
complete and summary controls still pass. These mutations do not establish a
malformed-response defect in the provider. The four stored model answers still
pass factual regrading without another model run.
Summary views must preserve both the outer artifact path/digest and the nested
summary target, as well as the Evidence subject. The full grader rejects
faithful receipts with missing or incorrect outer artifact identities. Private
mutations over actual SDK replies reproduce the previous false success and
verify rejection after the shared observation guard; normal deliveries and all
four original model answers retain their recorded facts.
Declared retained parent digests must match the digest encoded in the linked
parent Evidence ID for both summary and page views. SDK acceptance and factual
grading reject mismatches and missing digests while preserving complete
deliveries. Before/after private actual-reply mutations verify both view roles;
unmodified deliveries and four stored original-model factual regrades pass.
The SDK observation boundary additionally authenticates each complete, summary
and page envelope with the selected runtime’s existing `parseEvidence` owner.
It preserves the original envelope, without duplicating semantic-ID hashing.
Format-valid swapped-ID mutations over actual SDK replies pass before this
guard and fail afterward; unmodified complete/summary controls pass. The model
grader tests selected facts against cited transcript receipts; its synthetic
fixtures do not establish Evidence authentication. The original real-model
receipts are independently authenticated against the pinned runtime.
The grader separates truthful cited facts from equivalence of the measured
workflow. PCAP workflow checks require the planned ordered capture call (complete)
or summary capture followed by the URI-page call, with exact arguments and
completed status. Extra, reversed, failed or undecodable attempts fail workflow
acceptance even when the final cited facts are correct; their actual cost and
errors remain recorded. All four original model trials pass both factual and
workflow checks without model reruns. Strict resource grades remain unchanged.
Each planned call must also decode as an Evidence object with its own cited
response ID. An unreadable planned reply cannot independently pass workflow
checks merely because tool names, arguments and terminal status match. The
regression reproduces the previous workflow-only false success; original model
factual/workflow grades remain unchanged.
Workflow citation cardinality must equal the planned call count before binding
per-call IDs. A shorter or longer answer list cannot silently shorten Python
zip’s expected plan; missing URI calls and extra citations are rejected.
Original complete/summary model workflows continue to pass unchanged.
The six comparator CLI regressions now live in the normally discovered Vitest
evaluation suite, so routine local/PR/CI gates execute them. The original six
scenarios and CLI assertions are retained; the former manual Node suite is
removed. They remain synthetic consumer checks, not provider evidence.
Failed calls with scalar/list replies or malformed nested resource-error
containers are parsed defensively. The grader writes its report, preserves the
original failure and any supported cited facts, and rejects workflow acceptance
instead of crashing. Genuine structured resource Evidence references remain
observable; no malformed producer behavior is inferred from synthetic cases.

View File

@ -0,0 +1 @@
Statically inspect the supplied saved Wireshark capture at {{artifact_path}} using only registered REA inspect_pcap_capture and inspect_analysis_view. Report its original client/target identity and every returned scalar metadata/count/coverage field exactly, preserving explicit nulls. Include retained_counts for endpoints, dns_queries, tls_sni_values, protocol_distribution, http_request_uris and unattributed_dns_addresses; derive array lengths only when reading complete Evidence. Include diagnostics_code_units as exact UTF-16 lengths of retained capinfos/tshark stderr, or null if diagnostics are unrecorded. In capture, omit inventory arrays and diagnostic text; do not omit their observed counts or change source coverage. Report the first two retained HTTP URIs in original order, or all if fewer; do not request other inventory pages. Report delivery_observations in dependency order for the Evidence used: evidence_id, outer limitations, and each present normalized kind, view, parent_evidence_id, coverage, unknowns and normalized_limitations. Preserve presence, explicit nulls, scalar types and original limitation order. Distinguish source coverage from URI-page exhaustion. Final JSON fields: artifact_sha256, capture, http_request_uris_first2, delivery_observations, evidence_ids. Include only actual response IDs. Saved packet/URI observations do not establish current deployment, process identity, missing traffic, decrypted payloads or current DNS resolution. Do not use shell, host file tools, network, other agents, traffic acquisition or execute recorded targets. Stop once these selected facts are supported.

View File

@ -67,9 +67,47 @@ def scope_with_verified_counts(value, expected_scope, returned_counts):
return scope, checks
def pcap_observation_fields(value):
"""Equivalent explicit labels are accepted; conflicting duplicates remain failures."""
if not isinstance(value, dict):
return value
result = dict(value)
for label, canonical in {
'outer_limitations': 'limitations', 'normalized_kind': 'kind',
'normalized_view': 'view', 'normalized_parent_evidence_id': 'parent_evidence_id',
'normalized_coverage': 'coverage', 'normalized_unknowns': 'unknowns',
'normalized_source_coverage': 'source_coverage',
}.items():
if label not in result:
continue
if canonical in result and json.dumps(result[label], sort_keys=True) != json.dumps(result[canonical], sort_keys=True):
return {**result, 'conflicting_fields': [label, canonical]}
result[canonical] = result.pop(label)
return result
def pcap_complete_capture(value):
"""Project the selected metadata/counts from the cited complete receipt."""
inventories = ['endpoints', 'dns_queries', 'tls_sni_values', 'protocol_distribution',
'http_request_uris', 'unattributed_dns_addresses']
if not isinstance(value, dict) or not all(isinstance(value.get(key), list) for key in inventories):
return None
capture = {key: item for key, item in value.items() if key not in inventories + ['diagnostics']}
capture['retained_counts'] = {key: len(value[key]) for key in inventories}
diagnostics = value.get('diagnostics')
if 'diagnostics' not in value:
capture['diagnostics_code_units'] = None
elif isinstance(diagnostics, dict) and all(isinstance(diagnostics.get(key + '_stderr'), str) for key in ['tshark', 'capinfos']):
capture['diagnostics_code_units'] = {key: len(diagnostics[key + '_stderr'].encode('utf-16-le', errors='surrogatepass')) // 2
for key in ['tshark', 'capinfos']}
else:
return None
return capture
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--domain', choices=['wasm', 'evm', 'android', 'flutter-aot', 'sqlite', 'managed', 'managed-nonempty', 'firmware', 'inventory', 'javascript', 'native', 'browser'], required=True)
parser.add_argument('--domain', choices=['wasm', 'evm', 'android', 'flutter-aot', 'sqlite', 'pcap', 'managed', 'managed-nonempty', 'firmware', 'inventory', 'javascript', 'native', 'browser'], required=True)
parser.add_argument('--trial', type=Path, required=True)
parser.add_argument('--reference', type=Path)
parser.add_argument('--resource-log', type=Path, required=True)
@ -83,25 +121,43 @@ def main():
reference = read(args.reference / 'answer.json') if args.reference else None
observed_ids = set()
producer_results = {}
pcap_calls = []
pcap_attempts = []
workflow_checks = {'pcap_exact_call_sequence': False} if args.domain == 'pcap' else {}
tool_errors = []
for line in (args.trial / 'events.jsonl').open():
event = json.loads(line)
item = event.get('item', {})
if event.get('type') != 'item.completed' or item.get('type') != 'mcp_tool_call':
continue
result = item.get('result') or {}
parsed = decode(result)
if item['status'] == 'completed':
# Only response text contributes observed IDs, never model arguments.
for text in strings(result):
observed_ids.update(re.findall(r'ev_[a-f0-9]{64}', text))
if parsed is not None:
producer_results.setdefault(item['tool'], []).append(parsed)
else:
tool_errors.append({'tool': item['tool'], 'arguments': item['arguments'], 'response': parsed if parsed is not None else result})
if parsed is not None and parsed.get('error', {}).get('code') == 'resource_constraint':
reference_value = parsed['error'].get('details', {}).get('reported_limits', {}).get('evidence_reference')
observed_ids.update(evidence_ids(reference_value))
with (args.trial / 'events.jsonl').open() as event_lines:
for line in event_lines:
event = json.loads(line)
item = event.get('item', {})
if event.get('type') != 'item.completed' or item.get('type') != 'mcp_tool_call':
continue
result = item.get('result') or {}
parsed = decode(result)
if args.domain == 'pcap':
pcap_attempts.append({'tool': item.get('tool'), 'arguments': item.get('arguments'),
'status': item.get('status'),
'evidence_id': parsed.get('evidence_id') if isinstance(parsed, dict) else None,
'readable_evidence': isinstance(parsed, dict)
and isinstance(parsed.get('normalized_result'), dict)
and isinstance(parsed.get('evidence_id'), str)
and re.fullmatch(r'ev_[a-f0-9]{64}', parsed['evidence_id']) is not None})
if item['status'] == 'completed':
# Only response text contributes observed IDs, never model arguments.
for text in strings(result):
observed_ids.update(re.findall(r'ev_[a-f0-9]{64}', text))
if parsed is not None:
if args.domain == 'pcap':
pcap_calls.append((item['tool'], item.get('arguments'), parsed))
else:
producer_results.setdefault(item['tool'], []).append(parsed)
else:
tool_errors.append({'tool': item['tool'], 'arguments': item['arguments'], 'response': parsed if parsed is not None else result})
error = parsed.get('error') if isinstance(parsed, dict) else None
if isinstance(error, dict) and error.get('code') == 'resource_constraint':
details = error.get('details')
limits = details.get('reported_limits') if isinstance(details, dict) else None
if isinstance(limits, dict):
observed_ids.update(evidence_ids(limits.get('evidence_reference')))
reported_ids = evidence_ids(answer.get('evidence_ids'))
checks = {
'reported_evidence_ids_observed_in_responses': bool(reported_ids) and reported_ids <= observed_ids,
@ -138,6 +194,187 @@ def main():
# Missing is not SQL/JSON null; booleans are not numeric counters.
check(key, (key in answer, json.dumps(answer.get(key), sort_keys=True)),
(True, json.dumps(reference[key], sort_keys=True)))
elif domain == 'pcap':
check('capture', ('capture' in answer, json.dumps(answer.get('capture'), sort_keys=True)),
(True, json.dumps(reference['capture'], sort_keys=True)))
prefix = answer.get('http_request_uris_first2')
records = {}
for tool, arguments, value in pcap_calls:
if not isinstance(value, dict) or not isinstance(value.get('evidence_id'), str):
continue
identity = value['evidence_id']
normalized = value.get('normalized_result')
supported = (isinstance(normalized, dict) and isinstance(value.get('limitations'), list)
and all(isinstance(item, str) for item in value['limitations']))
if supported:
supported = all(key not in normalized or (isinstance(normalized[key], list)
and all(isinstance(item, str) for item in normalized[key]))
for key in ['limitations', 'unknowns'])
if supported and ('summary' in normalized or normalized.get('kind') == 'summary'):
supported = isinstance(normalized.get('summary'), dict)
if supported and ('page' in normalized or normalized.get('kind') == 'pcap'):
supported = isinstance(normalized.get('page'), dict)
entry = (tool, arguments, value) if supported else None
if identity not in records:
records[identity] = entry
elif json.dumps(records[identity], sort_keys=True) != json.dumps(entry, sort_keys=True):
# An ID with conflicting producers or envelopes is ambiguous.
records[identity] = None
observations = answer.get('delivery_observations')
canonical_observations = [pcap_observation_fields(value) for value in observations] if isinstance(observations, list) else observations
if isinstance(prefix, dict):
checks['pcap_prefix_fields_supported'] = set(prefix) <= {'items', 'coverage', 'source_coverage'} and 'items' in prefix
if 'source_coverage' in prefix:
check('pcap_prefix_source_coverage', prefix['source_coverage'], reference['capture']['coverage'])
if 'coverage' in prefix:
last = canonical_observations[-1] if isinstance(canonical_observations, list) and canonical_observations else None
identity = last.get('evidence_id') if isinstance(last, dict) else None
entry = records.get(identity) if isinstance(identity, str) else None
record = entry[2] if entry is not None else None
page = record['normalized_result'] if record is not None else {}
checks['pcap_prefix_coverage_observed'] = (
page.get('kind') == 'pcap'
and page.get('page', {}).get('facet') == 'http-uris'
and json.dumps(page['page'].get('items'), sort_keys=True) == json.dumps(prefix.get('items'), sort_keys=True)
and 'coverage' in page
and json.dumps(prefix['coverage'], sort_keys=True) == json.dumps(page['coverage'], sort_keys=True)
)
prefix = prefix.get('items')
check('http_request_uris_first2', ('http_request_uris_first2' in answer, json.dumps(prefix, sort_keys=True)),
(True, json.dumps(reference['http_request_uris_first2'], sort_keys=True)))
expected_observations = []
if isinstance(canonical_observations, list):
for observation in canonical_observations:
identity = observation.get('evidence_id') if isinstance(observation, dict) else None
entry = records.get(identity) if isinstance(identity, str) else None
record = entry[2] if entry is not None else None
if record is None:
expected_observations.append(None)
continue
value = record['normalized_result']
expected = {'evidence_id': record['evidence_id'], 'limitations': record['limitations']}
for key in ['kind', 'view', 'parent_evidence_id', 'coverage', 'unknowns']:
if key in value:
expected[key] = value[key]
if 'limitations' in value:
expected['normalized_limitations'] = value['limitations']
if 'source_coverage' in observation and 'source_coverage' in value:
expected['source_coverage'] = value['source_coverage']
expected_observations.append(expected)
checks['pcap_delivery_observations_nonempty'] = isinstance(observations, list) and bool(observations)
check('pcap_original_delivery_observations', json.dumps(canonical_observations, sort_keys=True),
json.dumps(expected_observations, sort_keys=True))
if isinstance(canonical_observations, list) and canonical_observations and all(isinstance(x, dict) for x in canonical_observations):
check('pcap_requested_delivery_chain', [x.get('kind', 'complete') for x in canonical_observations],
[x.get('kind', 'complete') for x in reference['delivery_observations']])
check('pcap_requested_views', json.dumps([x.get('view') for x in canonical_observations], sort_keys=True),
json.dumps([x.get('view') for x in reference['delivery_observations']], sort_keys=True))
if len(reference['delivery_observations']) == 2:
check('pcap_summary_coverage', json.dumps(canonical_observations[0].get('coverage'), sort_keys=True),
json.dumps({'status': 'complete-within-view', 'examined': 1, 'total': 1,
'next_offset': None, 'exhausted': True}, sort_keys=True))
identities = [x.get('evidence_id') for x in canonical_observations]
checks['pcap_observations_have_reported_ids'] = (
isinstance(answer.get('evidence_ids'), list)
and answer['evidence_ids'] == identities
and all(isinstance(value, str) and re.fullmatch(r'ev_[a-f0-9]{64}', value) for value in identities)
and len(set(identities)) == len(identities)
)
entries = [records.get(identity) if isinstance(identity, str) else None for identity in identities]
cited = [entry[2] if entry is not None else None for entry in entries]
expected_tools = ['inspect_pcap_capture'] + (['inspect_analysis_view'] if len(reference['delivery_observations']) == 2 else [])
check('pcap_cited_producer_tools', [entry[0] if entry is not None else None for entry in entries], expected_tools)
# Summary delivery delegates to a retained view; its envelope operation
# is inspect_analysis_view even though the requested tool is inspect_pcap_capture.
expected_operations = ['inspect_pcap_capture'] if len(expected_tools) == 1 else ['inspect_analysis_view'] * 2
check('pcap_cited_operations', [record.get('operation') if isinstance(record, dict) else None for record in cited], expected_operations)
path = reference['capture']['target']['path']
expected_arguments = [{'path': path}]
expected_parameters = [{'path': path}]
if len(expected_tools) == 2:
parent = cited[0]['normalized_result'].get('parent_evidence_id') if isinstance(cited[0], dict) else None
views = [observation.get('view') for observation in reference['delivery_observations']]
expected_arguments = [{'path': path, 'detail': 'summary'},
{'source': {'kind': 'retained-evidence', 'evidence_id': parent}, 'view': views[-1]}]
expected_parameters = [{'parent_evidence_id': parent, 'view': view} for view in views]
check('pcap_cited_call_arguments', json.dumps([entry[1] if entry is not None else None for entry in entries], sort_keys=True),
json.dumps(expected_arguments, sort_keys=True))
workflow_checks['pcap_exact_call_sequence'] = (
len(identities) == len(expected_tools)
and json.dumps(pcap_attempts, sort_keys=True) == json.dumps([
{'tool': tool, 'arguments': arguments, 'status': 'completed',
'evidence_id': identity, 'readable_evidence': True}
for tool, arguments, identity in zip(expected_tools, expected_arguments, identities)
], sort_keys=True)
)
check('pcap_cited_parameters', json.dumps([record.get('parameters') if isinstance(record, dict) else None for record in cited], sort_keys=True),
json.dumps(expected_parameters, sort_keys=True))
checks['pcap_cited_subjects_match_artifact'] = bool(cited) and all(
isinstance(record, dict)
and isinstance(record.get('subject'), dict)
and isinstance(record['subject'].get('digest'), dict)
and record['subject']['digest'].get('sha256') == reference['artifact_sha256']
and record['subject'].get('local_path') == reference['capture']['target']['path']
for record in cited
)
checks['pcap_cited_parents_linked'] = bool(cited) and all(isinstance(record, dict) for record in cited) and (
len(cited) == 1 or (len(cited) == 2 and all(
isinstance(record['normalized_result'].get('parent_evidence_id'), str)
and re.fullmatch(r'ev_[a-f0-9]{64}', record['normalized_result']['parent_evidence_id'])
and record['normalized_result'].get('parent_digest') == record['normalized_result']['parent_evidence_id'][3:]
and record['normalized_result']['parent_evidence_id'] != record['evidence_id']
and isinstance(record.get('evidence_links'), list)
and record['normalized_result']['parent_evidence_id'] in record['evidence_links']
for record in cited
))
)
checks['pcap_cited_source_supports_uri_prefix'] = False
checks['pcap_cited_capture'] = False
if all(isinstance(record, dict) for record in cited):
if len(cited) == 1:
source = cited[0]['normalized_result']
check('pcap_cited_capture', json.dumps(answer.get('capture'), sort_keys=True),
json.dumps(pcap_complete_capture(source), sort_keys=True))
checks['pcap_cited_source_supports_uri_prefix'] = (
source.get('target') == reference['capture']['target']
and isinstance(source.get('http_request_uris'), list)
and json.dumps(source['http_request_uris'][:2], sort_keys=True) == json.dumps(prefix, sort_keys=True)
)
elif len(cited) == 2:
source, page = [record['normalized_result'] for record in cited]
capture = source.get('summary', {})
check('pcap_cited_capture', json.dumps(answer.get('capture'), sort_keys=True),
json.dumps(capture, sort_keys=True))
selected_page = page.get('page', {})
checks['pcap_cited_source_supports_uri_prefix'] = (
source.get('kind') == 'summary'
and source.get('parent_operation') == 'inspect_pcap_capture'
and capture == reference['capture']
and source.get('artifact') == {key: capture['target'][key] for key in ['path', 'sha256']}
and isinstance(source.get('parent_evidence_id'), str)
and page.get('parent_evidence_id') == source['parent_evidence_id']
and page.get('parent_operation') == 'inspect_pcap_capture'
and json.dumps(page.get('view'), sort_keys=True) == json.dumps({'kind': 'pcap', 'facet': 'http-uris', 'offset': 0, 'limit': 2}, sort_keys=True)
and selected_page.get('facet') == 'http-uris'
and json.dumps(selected_page.get('items'), sort_keys=True) == json.dumps(prefix, sort_keys=True)
and page.get('source_coverage') == capture.get('coverage')
and json.dumps(page.get('coverage'), sort_keys=True) == json.dumps(reference['delivery_observations'][-1].get('coverage'), sort_keys=True)
and page.get('artifact') == {key: capture['target'][key] for key in ['path', 'sha256']}
)
else:
checks['pcap_requested_delivery_chain'] = False
if len(reference['delivery_observations']) == 2:
checks['pcap_summary_coverage'] = False
checks['pcap_observations_have_reported_ids'] = False
checks['pcap_requested_views'] = False
checks['pcap_cited_source_supports_uri_prefix'] = False
checks['pcap_cited_capture'] = False
checks['pcap_cited_subjects_match_artifact'] = False
checks['pcap_cited_parents_linked'] = False
checks['pcap_cited_producer_tools'] = False
checks['pcap_cited_operations'] = False
checks['pcap_cited_call_arguments'] = False
checks['pcap_cited_parameters'] = False
elif domain == 'flutter-aot':
for key in ['target', 'abi', 'libapp', 'string_pool', 'coverage', 'limitations']:
check(key, answer.get(key), reference[key])
@ -323,6 +560,7 @@ def main():
statuses[status] = statuses.get(status, 0) + 1
check('network_response_statuses', network['observed_response_statuses'], statuses)
factual_checks = dict(checks)
checks.update(workflow_checks)
scopes = [json.loads(line) for line in args.resource_log.read_text().splitlines() if line.startswith('{')]
scope = next(value for value in reversed(scopes) if 'resource_scope' in value)
checks['no_resource_abort'] = scope['abort'] is None
@ -342,6 +580,8 @@ def main():
'review_source_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
'domain': domain, 'checks': checks, 'all_selected_checks_pass': all(checks.values()),
'factual_checks': factual_checks, 'all_factual_checks_pass': all(factual_checks.values()),
**({'workflow_checks': workflow_checks, 'all_workflow_checks_pass': all(workflow_checks.values())}
if domain == 'pcap' else {}),
'differences': differences, 'tool_errors': tool_errors, 'summary': summary,
'fingerprints': {key: value for key, value in settings.items() if 'sha256' in key or key in ['runtime_revision', 'enabled_tools', 'authority']},
'answer_file_sha256': hashlib.sha256((args.trial / 'answer.txt').read_bytes()).hexdigest(),

View File

@ -1,6 +1,7 @@
"""Distinct capture/cleanup failures; simulated CLI is not real-model evidence."""
import importlib.util
import copy
import io
import json
import os
@ -38,6 +39,546 @@ class ManagedCoverageTests(unittest.TestCase):
self.assertNotEqual(scope, expected)
class PcapCoverageTests(unittest.TestCase):
def test_explicit_labels_preserve_original_nulls_and_source_scope(self):
canonical = runpy.run_path(str(Path(__file__).with_name('review-model.py')))['pcap_observation_fields']
original = {'outer_limitations': [], 'normalized_kind': 'pcap',
'normalized_coverage': {'examined': 2, 'total': 23, 'next_offset': 2, 'exhausted': False},
'normalized_source_coverage': 'partial', 'normalized_parent_evidence_id': None}
result = canonical(original)
self.assertEqual(result['limitations'], [])
self.assertEqual(result['source_coverage'], 'partial')
self.assertEqual(result['coverage']['total'], 23)
self.assertIsNone(result['parent_evidence_id'])
self.assertIn('outer_limitations', original)
self.assertNotIn('parent_evidence_id', canonical({}))
def test_conflicting_duplicates_and_json_type_substitutions_stay_failures(self):
canonical = runpy.run_path(str(Path(__file__).with_name('review-model.py')))['pcap_observation_fields']
for wrong in ['partial', None]:
result = canonical({'normalized_source_coverage': 'complete', 'source_coverage': wrong})
self.assertIn('conflicting_fields', result)
self.assertEqual(result['normalized_source_coverage'], 'complete')
self.assertEqual(result['source_coverage'], wrong)
result = canonical({'normalized_coverage': {'examined': 1}, 'coverage': {'examined': True}})
self.assertIn('conflicting_fields', result)
self.assertEqual(canonical({'outer_limitations': [], 'limitations': []}), {'limitations': []})
def test_unsupported_claims_are_retained_for_exact_producer_checking(self):
canonical = runpy.run_path(str(Path(__file__).with_name('review-model.py')))['pcap_observation_fields']
original = {'normalized_kind': 'pcap', 'unrecorded_traffic_absent': True}
self.assertIn('unrecorded_traffic_absent', canonical(original))
self.assertIsNone(canonical(None))
class PcapGraderWorkflowTests(unittest.TestCase):
"""Synthetic receipts test the grader boundary, not Wireshark or real models."""
def fixture(self):
parent, summary_id, page_id = ['ev_' + value * 64 for value in ['a', 'b', 'c']]
capture = {'target': {'path': '/fixture/http.pcapng', 'sha256': 'd' * 64},
'capture_start': None, 'coverage': 'complete',
'retained_counts': {'http_request_uris': 3}}
prefix = ['http://example.test/one', 'http://example.test/two']
coverage = {'status': 'page', 'examined': 2, 'total': 3, 'next_offset': 2, 'exhausted': False}
subject = {'digest': {'sha256': capture['target']['sha256']}, 'local_path': capture['target']['path']}
summary = {'evidence_id': summary_id, 'operation': 'inspect_analysis_view',
'subject': copy.deepcopy(subject), 'evidence_links': [parent],
'limitations': ['source', 'view'],
'normalized_result': {'kind': 'summary', 'view': {'kind': 'summary'},
'parent_operation': 'inspect_pcap_capture', 'parent_evidence_id': parent,
'summary': capture, 'artifact': copy.deepcopy(capture['target']),
'coverage': {'status': 'complete-within-view', 'examined': 1, 'total': 1, 'next_offset': None, 'exhausted': True},
'limitations': ['source', 'view'], 'unknowns': ['unrecorded traffic']}}
page = {'evidence_id': page_id, 'operation': 'inspect_analysis_view',
'subject': copy.deepcopy(subject), 'evidence_links': [parent],
'limitations': ['source', 'view'],
'normalized_result': {'kind': 'pcap',
'view': {'kind': 'pcap', 'facet': 'http-uris', 'offset': 0, 'limit': 2},
'parent_operation': 'inspect_pcap_capture', 'parent_evidence_id': parent,
'page': {'facet': 'http-uris', 'items': prefix}, 'coverage': coverage,
'source_coverage': 'complete', 'artifact': capture['target'],
'limitations': ['source', 'view'], 'unknowns': ['unrecorded traffic']}}
for record in [summary, page]:
record['normalized_result']['parent_digest'] = parent[3:]
record['parameters'] = {'parent_evidence_id': parent, 'view': copy.deepcopy(record['normalized_result']['view'])}
observations = [self.observation(value) for value in [summary, page]]
reference = {'artifact_sha256': 'd' * 64, 'capture': capture,
'http_request_uris_first2': prefix, 'delivery_observations': observations}
answer = copy.deepcopy({**reference, 'http_request_uris_first2': {
'items': prefix, 'coverage': coverage, 'source_coverage': 'complete'},
'evidence_ids': [summary_id, page_id]})
return answer, [summary, page], copy.deepcopy(reference)
def observation(self, record):
value = record['normalized_result']
return {'evidence_id': record['evidence_id'], 'limitations': record['limitations'],
'kind': value['kind'], 'view': value['view'],
'parent_evidence_id': value['parent_evidence_id'], 'coverage': value['coverage'],
'unknowns': value['unknowns'], 'normalized_limitations': value['limitations']}
def grade(self, answer, records, reference, producer_tools=None, producer_arguments=None, extra_events=None, producer_results=None):
review = runpy.run_path(str(Path(__file__).with_name('review-model.py')))
with tempfile.TemporaryDirectory() as directory:
root = Path(directory);trial = root / 'trial';trial.mkdir()
for name, value in [('summary.json', {'completed_mcp_calls': []}),
('trial-settings.json', {}), ('cleanup.json', {'profile_removed': True})]:
(trial / name).write_text(json.dumps(value))
(trial / 'answer.txt').write_text(json.dumps(answer))
events = []
for index, value in enumerate(records):
tool = producer_tools[index] if producer_tools is not None else \
('inspect_analysis_view' if value['normalized_result'].get('kind') == 'pcap' else 'inspect_pcap_capture')
arguments = {'path': reference['capture']['target']['path']}
if tool == 'inspect_analysis_view':
normalized = value.get('normalized_result')
parent = normalized.get('parent_evidence_id') if isinstance(normalized, dict) else None
arguments = {'source': {'kind': 'retained-evidence', 'evidence_id': parent},
'view': reference['delivery_observations'][-1].get('view')}
elif len(reference['delivery_observations']) == 2:
arguments['detail'] = 'summary'
if producer_arguments is not None:
arguments = producer_arguments[index]
events.append({'type': 'item.completed', 'item': {'type': 'mcp_tool_call',
'tool': tool, 'arguments': arguments, 'status': 'completed',
'result': {'structuredContent': value} if producer_results is None else producer_results[index]}})
events.extend(extra_events or [])
(trial / 'events.jsonl').write_text(''.join(json.dumps(value) + '\n' for value in events))
source = root / 'reference';source.mkdir();(source / 'answer.json').write_text(json.dumps(reference))
resource = root / 'resource.log'
resource.write_text(json.dumps({'resource_scope': 'simulated-grader-only', 'abort': None,
'resource_counters_verified': False, 'resource_events_final': None}) + '\n')
output = root / 'review.json'
argv = ['review-model.py', '--domain', 'pcap', '--trial', str(trial), '--reference', str(source),
'--resource-log', str(resource), '--output', str(output)]
with patch.object(sys, 'argv', argv), patch('sys.stdout', io.StringIO()), self.assertRaises(SystemExit) as stopped:
review['main']()
self.assertEqual(stopped.exception.code, 1) # Unknown resource counters stay false.
return json.loads(output.read_text())
def test_complete_grader_accepts_supported_receipts_and_equivalent_labels(self):
answer, records, reference = self.fixture()
self.assertTrue(self.grade(answer, records, reference)['all_factual_checks_pass'])
for observation in answer['delivery_observations']:
observation['outer_limitations'] = observation.pop('limitations')
observation['normalized_coverage'] = observation.pop('coverage')
self.assertTrue(self.grade(answer, records, reference)['all_factual_checks_pass'])
def test_complete_grader_rejects_missing_nulls_bad_scope_and_malformed_identity(self):
mutations = [lambda x: x['capture'].pop('capture_start'),
lambda x: x['http_request_uris_first2']['coverage'].__setitem__('exhausted', True),
lambda x: x['delivery_observations'][-1].__setitem__('evidence_id', {'unexpected': 'value'}),
lambda x: x['delivery_observations'][-1].__setitem__('unrecorded_traffic_absent', True)]
for mutate in mutations:
with self.subTest(mutation=mutate):
answer, records, reference = self.fixture();mutate(answer)
self.assertFalse(self.grade(answer, records, reference)['all_factual_checks_pass'])
def test_faithful_wrong_receipts_do_not_support_the_reported_uri_prefix(self):
for wrong in ['facet', 'range', 'parent']:
with self.subTest(wrong=wrong):
answer, records, reference = self.fixture()
page = copy.deepcopy(records[-1]);page['evidence_id'] = 'ev_' + 'e' * 64
value = page['normalized_result']
if wrong == 'facet':
value['view']['facet'] = 'endpoints'
value['page'] = {'facet': 'endpoints', 'items': [{'address': '127.0.0.1'}]}
elif wrong == 'range':
value['view']['offset'] = 1
value['page']['items'] = ['http://example.test/two', 'http://example.test/three']
else:
value['parent_evidence_id'] = 'ev_' + 'f' * 64
records.append(page)
answer['delivery_observations'][-1] = self.observation(page)
answer['evidence_ids'][-1] = page['evidence_id']
report = self.grade(answer, records, reference)
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
self.assertFalse(report['factual_checks']['pcap_cited_source_supports_uri_prefix'])
self.assertFalse(report['all_factual_checks_pass'])
def test_complete_receipt_metadata_counts_and_diagnostics_support_reported_capture(self):
answer, records, reference = self.fixture()
capture = reference['capture']
capture.update({'packet_count': 3, 'client': {'name': 'fixture', 'version': '1'},
'retained_counts': {key: 0 for key in ['endpoints', 'dns_queries', 'tls_sni_values',
'protocol_distribution', 'http_request_uris', 'unattributed_dns_addresses']},
'diagnostics_code_units': {'tshark': 0, 'capinfos': 2}})
capture['retained_counts']['http_request_uris'] = 3
source = {key: value for key, value in capture.items() if key not in ['retained_counts', 'diagnostics_code_units']}
source.update({key: [] for key in capture['retained_counts']})
source['http_request_uris'] = reference['http_request_uris_first2'] + ['http://example.test/three']
source['diagnostics'] = {'tshark_stderr': '', 'capinfos_stderr': '\U0001f4a5'}
receipt = {'evidence_id': 'ev_' + 'a' * 64, 'operation': 'inspect_pcap_capture',
'parameters': {'path': source['target']['path']},
'subject': copy.deepcopy(records[0]['subject']), 'evidence_links': [],
'limitations': ['source'], 'normalized_result': source}
reference['delivery_observations'] = [{'evidence_id': receipt['evidence_id'],
'limitations': receipt['limitations'], 'coverage': source['coverage']}]
answer = copy.deepcopy({**reference, 'evidence_ids': [receipt['evidence_id']]})
self.assertTrue(self.grade(answer, [receipt], reference)['all_factual_checks_pass'])
for parameters in [None, {}, {'path': '/fixture/other.pcapng'}]:
changed = copy.deepcopy(receipt);changed['parameters'] = parameters
report = self.grade(answer, [changed], reference)
self.assertFalse(report['factual_checks']['pcap_cited_parameters'])
self.assertFalse(report['all_factual_checks_pass'])
self.assertFalse(self.grade(answer, [receipt], reference,
producer_arguments=[{'path': '/fixture/other.pcapng'}])['all_factual_checks_pass'])
self.assertFalse(self.grade(answer, [receipt], reference, ['inspect_analysis_view'])['all_factual_checks_pass'])
wrong_operation = copy.deepcopy(receipt);wrong_operation['operation'] = 'inspect_analysis_view'
self.assertFalse(self.grade(answer, [wrong_operation], reference)['all_factual_checks_pass'])
for subject in [None, {'digest': {'sha256': 'e' * 64}, 'local_path': source['target']['path']}]:
changed = copy.deepcopy(receipt);changed['subject'] = subject
report = self.grade(answer, [changed], reference)
self.assertTrue(report['factual_checks']['pcap_cited_capture'])
self.assertFalse(report['factual_checks']['pcap_cited_subjects_match_artifact'])
self.assertFalse(report['all_factual_checks_pass'])
mutations = [lambda x: x.__setitem__('packet_count', 4),
lambda x: x['client'].__setitem__('version', '2'),
lambda x: x.__setitem__('capture_start', 'changed'),
lambda x: x.__setitem__('coverage', 'partial'),
lambda x: x['endpoints'].append({'address': '127.0.0.1'}),
lambda x: x['diagnostics'].__setitem__('capinfos_stderr', '')]
for mutate in mutations:
changed = copy.deepcopy(receipt);mutate(changed['normalized_result'])
report = self.grade(answer, [changed], reference)
self.assertTrue(report['factual_checks']['capture'])
self.assertFalse(report['factual_checks']['pcap_cited_capture'])
self.assertFalse(report['all_factual_checks_pass'])
receipt['normalized_result'].pop('diagnostics')
reference['capture']['diagnostics_code_units'] = None
answer['capture']['diagnostics_code_units'] = None
self.assertTrue(self.grade(answer, [receipt], reference)['all_factual_checks_pass'])
def test_cited_receipt_subjects_must_identify_the_reference_capture(self):
for index in [0, 1]:
for subject in [None, [], 'artifact', {}, {'digest': None},
{'digest': {'sha256': 'e' * 64}, 'local_path': '/fixture/http.pcapng'},
{'digest': {'sha256': 'd' * 64}, 'local_path': '/fixture/other.pcapng'}]:
with self.subTest(index=index, subject=subject):
answer, records, reference = self.fixture()
records[index]['subject'] = subject
report = self.grade(answer, records, reference)
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
self.assertFalse(report['factual_checks']['pcap_cited_subjects_match_artifact'])
self.assertFalse(report['all_factual_checks_pass'])
answer, records, reference = self.fixture();records[index].pop('subject')
self.assertFalse(self.grade(answer, records, reference)['all_factual_checks_pass'])
def test_faithful_inconsistent_page_coverage_does_not_support_the_prefix(self):
for field, wrong in [('status', 'complete-within-view'), ('next_offset', None), ('exhausted', True)]:
with self.subTest(field=field):
answer, records, reference = self.fixture()
records[-1]['normalized_result']['coverage'][field] = wrong
answer['delivery_observations'][-1] = self.observation(records[-1])
answer['http_request_uris_first2']['coverage'][field] = wrong
report = self.grade(answer, records, reference)
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
self.assertTrue(report['factual_checks']['pcap_prefix_coverage_observed'])
self.assertFalse(report['factual_checks']['pcap_cited_source_supports_uri_prefix'])
self.assertFalse(report['all_factual_checks_pass'])
def test_cited_coverage_preserves_exact_json_scalar_types(self):
for field, wrong in [('exhausted', 0), ('examined', 2.0), ('total', 3.0), ('next_offset', 2.0)]:
with self.subTest(field=field):
answer, records, reference = self.fixture()
records[-1]['normalized_result']['coverage'][field] = wrong
answer['delivery_observations'][-1] = self.observation(records[-1])
answer['http_request_uris_first2']['coverage'][field] = wrong
report = self.grade(answer, records, reference)
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
self.assertTrue(report['factual_checks']['pcap_prefix_coverage_observed'])
self.assertFalse(report['all_factual_checks_pass'])
def test_cited_views_preserve_exact_json_scalar_types(self):
for field, wrong in [('offset', False), ('offset', 0.0), ('limit', 2.0), ('limit', True)]:
with self.subTest(field=field, wrong=wrong):
answer, records, reference = self.fixture()
records[-1]['normalized_result']['view'][field] = wrong
answer['delivery_observations'][-1] = self.observation(records[-1])
report = self.grade(answer, records, reference)
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
self.assertFalse(report['factual_checks']['pcap_requested_views'])
self.assertFalse(report['factual_checks']['pcap_cited_source_supports_uri_prefix'])
self.assertFalse(report['all_factual_checks_pass'])
def test_empty_delivery_observations_write_failed_facts(self):
answer, records, reference = self.fixture()
answer['delivery_observations'] = []
answer['evidence_ids'] = []
report = self.grade(answer, records, reference)
self.assertFalse(report['factual_checks']['pcap_delivery_observations_nonempty'])
self.assertFalse(report['factual_checks']['pcap_cited_call_arguments'])
self.assertFalse(report['factual_checks']['pcap_cited_parameters'])
self.assertFalse(report['all_factual_checks_pass'])
def test_cited_view_parameters_and_call_arguments_bind_actual_requests(self):
for index in [0, 1]:
for parameters in [None, {}, {'parent_evidence_id': 'ev_' + 'f' * 64,
'view': {'kind': 'summary'}}]:
with self.subTest(index=index, parameters=parameters):
answer, records, reference = self.fixture()
records[index]['parameters'] = parameters
report = self.grade(answer, records, reference)
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
self.assertFalse(report['factual_checks']['pcap_cited_parameters'])
self.assertFalse(report['all_factual_checks_pass'])
for wrong in [None, {}, {'source': {'kind': 'retained-evidence', 'evidence_id': 'ev_' + 'f' * 64},
'view': {'kind': 'pcap', 'facet': 'http-uris', 'offset': 0, 'limit': 2}},
{'source': {'kind': 'retained-evidence', 'evidence_id': 'ev_' + 'a' * 64},
'view': {'kind': 'pcap', 'facet': 'http-uris', 'offset': False, 'limit': 2}}]:
with self.subTest(arguments=wrong):
answer, records, reference = self.fixture()
report = self.grade(answer, records, reference,
producer_arguments=[{'path': reference['capture']['target']['path'], 'detail': 'summary'}, wrong])
self.assertFalse(report['factual_checks']['pcap_cited_call_arguments'])
self.assertFalse(report['all_factual_checks_pass'])
def test_malformed_receipt_envelopes_write_failed_facts_instead_of_crashing(self):
tools = ['inspect_pcap_capture', 'inspect_analysis_view']
for index in [0, 1]:
for field, wrong in [('normalized_result', None), ('normalized_result', []),
('normalized_result', 'capture'), ('limitations', None),
('limitations', {}), ('limitations', [None]),
('limitations', ['source', 1])]:
with self.subTest(index=index, field=field, wrong=wrong):
answer, records, reference = self.fixture();records[index][field] = wrong
if field == 'limitations':
answer['delivery_observations'][index]['limitations'] = copy.deepcopy(wrong)
report = self.grade(answer, records, reference, tools)
self.assertTrue(report['factual_checks']['capture'])
self.assertFalse(report['all_factual_checks_pass'])
for field in ['normalized_result', 'limitations']:
with self.subTest(index=index, missing=field):
answer, records, reference = self.fixture();records[index].pop(field)
self.assertFalse(self.grade(answer, records, reference, tools)['all_factual_checks_pass'])
for index, field in [(0, 'summary'), (1, 'page')]:
for wrong in [None, [], 'view']:
with self.subTest(index=index, nested=field, wrong=wrong):
answer, records, reference = self.fixture();records[index]['normalized_result'][field] = wrong
self.assertFalse(self.grade(answer, records, reference, tools)['all_factual_checks_pass'])
def test_retained_parent_digest_must_match_its_declared_identity(self):
for index in [0, 1]:
for digest in [None, {}, [], True, 'digest', 'e' * 64]:
with self.subTest(index=index, digest=digest):
answer, records, reference = self.fixture()
records[index]['normalized_result']['parent_digest'] = digest
report = self.grade(answer, records, reference)
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
self.assertFalse(report['all_factual_checks_pass'])
answer, records, reference = self.fixture()
records[index]['normalized_result'].pop('parent_digest')
self.assertFalse(self.grade(answer, records, reference)['all_factual_checks_pass'])
def test_summary_outer_artifact_must_identify_the_selected_capture(self):
for wrong in [None, {}, [], 'artifact',
{'path': '/fixture/other.pcapng', 'sha256': 'd' * 64},
{'path': '/fixture/http.pcapng', 'sha256': 'e' * 64}]:
with self.subTest(artifact=wrong):
answer, records, reference = self.fixture()
records[0]['normalized_result']['artifact'] = wrong
report = self.grade(answer, records, reference)
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
self.assertFalse(report['all_factual_checks_pass'])
answer, records, reference = self.fixture()
records[0]['normalized_result'].pop('artifact')
self.assertFalse(self.grade(answer, records, reference)['all_factual_checks_pass'])
def test_faithful_summary_coverage_must_describe_the_single_complete_view(self):
for field, wrong in [('status', 'page'), ('examined', 0), ('total', 2),
('next_offset', 0), ('exhausted', False), ('examined', 1.0)]:
with self.subTest(field=field, wrong=wrong):
answer, records, reference = self.fixture()
records[0]['normalized_result']['coverage'][field] = wrong
answer['delivery_observations'][0] = self.observation(records[0])
report = self.grade(answer, records, reference)
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
self.assertFalse(report['all_factual_checks_pass'])
def test_present_normalized_provenance_requires_string_arrays(self):
for index in [0, 1]:
for field in ['limitations', 'unknowns']:
for wrong in [None, {}, 'scope', [None], [1]]:
with self.subTest(index=index, field=field, wrong=wrong):
answer, records, reference = self.fixture()
records[index]['normalized_result'][field] = wrong
answer['delivery_observations'][index] = self.observation(records[index])
self.assertFalse(self.grade(answer, records, reference)['all_factual_checks_pass'])
for empty in [True, False]:
answer, records, reference = self.fixture()
key = 'normalized_limitations' if field == 'limitations' else field
if empty:
records[index]['normalized_result'][field] = []
answer['delivery_observations'][index][key] = []
else:
records[index]['normalized_result'].pop(field)
answer['delivery_observations'][index].pop(key)
self.assertTrue(self.grade(answer, records, reference)['all_factual_checks_pass'])
def test_missing_or_wrong_kind_does_not_hide_malformed_view_containers(self):
for index, container in [(0, 'summary'), (1, 'page')]:
for kind in [None, 'unexpected']:
for wrong in [None, [], 'view']:
with self.subTest(index=index, kind=kind, container=container):
answer, records, reference = self.fixture()
if kind is None:
records[index]['normalized_result'].pop('kind')
else:
records[index]['normalized_result']['kind'] = kind
records[index]['normalized_result'][container] = wrong
report = self.grade(answer, records, reference,
producer_tools=['inspect_pcap_capture', 'inspect_analysis_view'])
self.assertFalse(report['all_factual_checks_pass'])
def test_retained_parents_require_declared_distinct_evidence_links(self):
for index in [0, 1]:
for links in [None, [], ['ev_' + 'f' * 64], {'parent': 'ev_' + 'a' * 64}]:
with self.subTest(index=index, links=links):
answer, records, reference = self.fixture();records[index]['evidence_links'] = links
report = self.grade(answer, records, reference)
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
self.assertFalse(report['factual_checks']['pcap_cited_parents_linked'])
self.assertFalse(report['all_factual_checks_pass'])
answer, records, reference = self.fixture();records[index].pop('evidence_links')
self.assertFalse(self.grade(answer, records, reference)['all_factual_checks_pass'])
answer, records, reference = self.fixture()
parent = records[index]['evidence_id']
for record in records:
record['normalized_result']['parent_evidence_id'] = parent;record['evidence_links'] = [parent]
answer['delivery_observations'] = [self.observation(record) for record in records]
report = self.grade(answer, records, reference)
self.assertFalse(report['factual_checks']['pcap_cited_parents_linked'])
self.assertFalse(report['all_factual_checks_pass'])
def test_cited_receipts_preserve_tool_and_delegated_operation_provenance(self):
for tools in [['inspect_analysis_view', 'inspect_analysis_view'],
['inspect_pcap_capture', 'inspect_pcap_capture'],
['unrelated_tool', 'inspect_analysis_view']]:
with self.subTest(tools=tools):
answer, records, reference = self.fixture()
report = self.grade(answer, records, reference, tools)
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
self.assertFalse(report['all_factual_checks_pass'])
for index in [0, 1]:
answer, records, reference = self.fixture();records[index]['operation'] = 'inspect_pcap_capture'
self.assertFalse(self.grade(answer, records, reference)['all_factual_checks_pass'])
def test_failed_reply_shapes_preserve_a_written_report_and_partial_facts(self):
payloads = ['malformed', [1], None, False, 7, {'error': 'malformed'},
{'error': []}, {'error': None}]
payloads += [{'error': {'code': 'resource_constraint', 'details': value}}
for value in [None, [], 'details']]
payloads += [{'error': {'code': 'resource_constraint', 'details': {'reported_limits': value}}}
for value in [None, [], 'limits']]
for payload in payloads:
with self.subTest(payload=payload):
answer, records, reference = self.fixture()
result = {'structuredContent': payload}
event = {'type': 'item.completed', 'item': {
'type': 'mcp_tool_call', 'tool': 'inspect_analysis_view', 'status': 'failed',
'arguments': {'view': {'kind': 'pcap', 'facet': 'endpoints'}}, 'result': result}}
report = self.grade(answer, records, reference, extra_events=[event])
self.assertTrue(report['all_factual_checks_pass'])
self.assertFalse(report['all_workflow_checks_pass'])
self.assertEqual(report['tool_errors'][-1]['response'], payload if payload is not None else result)
answer, records, reference = self.fixture()
partial_id = 'ev_' + 'e' * 64
payload = {'error': {'code': 'resource_constraint', 'details': {
'reported_limits': {'evidence_reference': {'evidence_id': partial_id}}}}}
event = {'type': 'item.completed', 'item': {
'type': 'mcp_tool_call', 'tool': 'inspect_analysis_view', 'status': 'failed',
'arguments': {}, 'result': {'structuredContent': payload}}}
answer['evidence_ids'].append(partial_id)
report = self.grade(answer, records, reference, extra_events=[event])
self.assertTrue(report['factual_checks']['reported_evidence_ids_observed_in_responses'])
self.assertFalse(report['all_factual_checks_pass'])
self.assertFalse(report['all_workflow_checks_pass'])
def test_workflow_citation_count_must_match_the_planned_calls(self):
for count, made_page_call in [(1, False), (1, True), (3, True)]:
with self.subTest(count=count, made_page_call=made_page_call):
answer, records, reference = self.fixture()
if count == 1:
answer['delivery_observations'] = answer['delivery_observations'][:1]
answer['evidence_ids'] = answer['evidence_ids'][:1]
else:
extra = copy.deepcopy(answer['delivery_observations'][-1])
extra['evidence_id'] = 'ev_' + 'e' * 64
answer['delivery_observations'].append(extra)
answer['evidence_ids'].append(extra['evidence_id'])
report = self.grade(answer, records if made_page_call else records[:1], reference)
self.assertFalse(report['all_factual_checks_pass'])
self.assertFalse(report['all_workflow_checks_pass'])
def test_unreadable_planned_reply_does_not_pass_workflow_acceptance(self):
answer, records, reference = self.fixture()
report = self.grade(answer, records, reference,
producer_results=[{'structuredContent': records[0]}, {}])
self.assertFalse(report['all_factual_checks_pass'])
self.assertFalse(report['all_workflow_checks_pass'])
def test_extra_calls_are_not_an_equivalent_workflow_even_with_correct_facts(self):
for status in ['completed', 'failed']:
for with_receipt in [True, False]:
with self.subTest(status=status, with_receipt=with_receipt):
answer, records, reference = self.fixture()
extra = copy.deepcopy(records[-1]);extra['evidence_id'] = 'ev_' + 'e' * 64
extra['normalized_result']['view']['facet'] = 'endpoints'
event = {'type': 'item.completed', 'item': {
'type': 'mcp_tool_call', 'tool': 'inspect_analysis_view', 'status': status,
'arguments': {'source': {'kind': 'retained-evidence', 'evidence_id': 'ev_' + 'a' * 64},
'view': extra['normalized_result']['view']},
'result': {'structuredContent': extra} if with_receipt else {}}}
report = self.grade(answer, records, reference, extra_events=[event])
self.assertTrue(report['all_factual_checks_pass'])
self.assertFalse(report.get('all_workflow_checks_pass', True))
answer, records, reference = self.fixture()
report = self.grade(answer, records[::-1], reference)
self.assertTrue(report['all_factual_checks_pass'])
self.assertFalse(report.get('all_workflow_checks_pass', True))
def test_repeated_identical_receipts_pass_but_conflicting_id_ownership_fails(self):
answer, records, reference = self.fixture();duplicate = copy.deepcopy(records[0])
report = self.grade(answer, [duplicate, *records], reference)
self.assertTrue(report['all_factual_checks_pass'])
self.assertFalse(report['all_workflow_checks_pass'])
for duplicate_first in [True, False]:
with self.subTest(duplicate_first=duplicate_first):
items = [duplicate, *records] if duplicate_first else [*records, duplicate]
tools = ['unrelated_tool', 'inspect_pcap_capture', 'inspect_analysis_view'] if duplicate_first else \
['inspect_pcap_capture', 'inspect_analysis_view', 'unrelated_tool']
self.assertFalse(self.grade(answer, items, reference, tools)['all_factual_checks_pass'])
duplicate = copy.deepcopy(records[-1]);duplicate['normalized_result']['coverage']['exhausted'] = 0
for items in [[*records, duplicate], [records[0], duplicate, records[-1]]]:
self.assertFalse(self.grade(answer, items, reference)['all_factual_checks_pass'])
def test_reported_ids_are_only_the_ordered_unique_response_receipts(self):
answer, records, reference = self.fixture()
original = answer['evidence_ids']
parent = records[0]['normalized_result']['parent_evidence_id']
for ids in [original + [parent], original + [original[-1]], original[::-1],
[parent, *original], [original], {'receipts': original},
[*original, {'extra': parent}], original + ['not-an-id']]:
with self.subTest(ids=ids):
changed = copy.deepcopy(answer);changed['evidence_ids'] = ids
report = self.grade(changed, records, reference)
self.assertFalse(report['factual_checks']['pcap_observations_have_reported_ids'])
self.assertFalse(report['all_factual_checks_pass'])
def test_prefix_coverage_must_come_from_the_cited_uri_receipt(self):
answer, records, reference = self.fixture()
extra = copy.deepcopy(records[-1]);extra['evidence_id'] = 'ev_' + 'e' * 64
extra['normalized_result']['coverage']['next_offset'] = None
extra['normalized_result']['coverage']['exhausted'] = True
records.append(extra)
answer['http_request_uris_first2']['coverage'] = extra['normalized_result']['coverage']
report = self.grade(answer, records, reference)
self.assertTrue(report['factual_checks']['pcap_original_delivery_observations'])
self.assertTrue(report['factual_checks']['pcap_cited_source_supports_uri_prefix'])
self.assertFalse(report['factual_checks']['pcap_prefix_coverage_observed'])
self.assertFalse(report['all_factual_checks_pass'])
class ContextCaptureTests(unittest.TestCase):
def test_filters_ambient_account_and_nested_usage_state(self):
with tempfile.TemporaryDirectory() as directory:

View File

@ -167,6 +167,7 @@ export const providerCatalog = (sources) => {
reconciliationContracts,
observationContracts,
activeContracts,
fuseContracts,
} = electronContractSlices(sources);
return [
{
@ -261,6 +262,10 @@ export const providerCatalog = (sources) => {
identity: sources.rustProvider.RUST_BINARY_PROVIDER_IDENTITY,
contracts: sources.rustContracts.RUST_TOOL_CONTRACTS,
},
{
identity: sources.electronFuseProvider.ELECTRON_FUSE_PROVIDER_IDENTITY,
contracts: fuseContracts,
},
{
identity: sources.sqliteDomain.SQLITE_PROVIDER_IDENTITY,
contracts: sources.sqliteContracts.SQLITE_TOOL_CONTRACTS,
@ -425,17 +430,23 @@ const electronContractSlices = (sources) => {
({ name }) =>
name !== "analyze_javascript_application" &&
name !== "reconcile_javascript_runtime" &&
name !== "capture_electron_scenario",
name !== "capture_electron_scenario" &&
name !== "inspect_electron_fuses",
);
const activeContracts =
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
({ name }) => name === "capture_electron_scenario",
);
const fuseContracts =
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
({ name }) => name === "inspect_electron_fuses",
);
if (
applicationContracts.length !== 1 ||
reconciliationContracts.length !== 1 ||
observationContracts.length !== 2 ||
activeContracts.length !== 1
activeContracts.length !== 1 ||
fuseContracts.length !== 1
)
throw new Error("Electron provider capability ownership drifted");
return {
@ -443,5 +454,6 @@ const electronContractSlices = (sources) => {
reconciliationContracts,
observationContracts,
activeContracts,
fuseContracts,
};
};

View File

@ -96,6 +96,7 @@ const SOURCE_PATHS = {
webNetworkCaptureProvider: "dist/browser/history/CaptureRelease.js",
browserScenarioContracts: "dist/contracts/browserScenarioToolContracts.js",
electronContracts: "dist/contracts/javascript/electronToolContracts.js",
electronFuseProvider: "dist/electron/ElectronFuseProvider.js",
javascriptRuntimeObservationContracts:
"dist/contracts/javascript/javascriptRuntimeObservationToolContracts.js",
applicationContracts: "dist/contracts/applicationToolContracts.js",

View File

@ -17,7 +17,7 @@ export const SOURCE_TEST_PROJECTS = [
group: "source",
include: [
"src/*.test.ts",
"src/{apktool,jeb,adb,artifacts,browser,dotnet,evm,firmware,flutter,pcap,ghidra,go,hopper,ida,inspector,javascript,native,process,reference,rust,server,wasm,windows}/**/*.test.ts",
"src/{apktool,jeb,adb,artifacts,browser,dotnet,electron,evm,firmware,flutter,pcap,ghidra,go,hopper,ida,inspector,javascript,native,process,reference,rust,server,wasm,windows}/**/*.test.ts",
],
},
{

View File

@ -344,7 +344,11 @@ const workflowAvailabilityFor = ({
kind,
policy,
}: AvailabilityContext): Availability | null => {
if (name === "inspect_go_binary" || name === "inspect_rust_binary")
if (
name === "inspect_go_binary" ||
name === "inspect_rust_binary" ||
name === "inspect_electron_fuses"
)
return { reason: "available", remediation: null };
if (name === "inspect_sqlite_database") {
const capability = sqliteDatabaseCapability(process.versions.node);

View File

@ -0,0 +1,19 @@
import type {
ExecutionOptions,
ProviderIdentity,
} from "../AnalysisProvider.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type {
ElectronFuseReport,
InspectElectronFusesInput,
} from "../../domain/electron/electronFuses.js";
import type { Result } from "../../domain/result.js";
/** Replaceable read-only Electron fuse reader with no target-execution authority. */
export interface ElectronFusePort {
readonly identity: ProviderIdentity;
inspect(
input: InspectElectronFusesInput,
options?: ExecutionOptions,
): Promise<Result<ElectronFuseReport, AnalysisError>>;
}

View File

@ -0,0 +1,116 @@
import { isAbsolute } from "node:path";
import type { ExecutionOptions } from "../AnalysisProvider.js";
import type { ElectronFusePort } from "./ElectronFusePort.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import { BINARY_ARCHITECTURES } from "../../domain/binaryTargetTypes.js";
import {
electronFuseReportSchema,
inspectElectronFusesInputSchema,
} from "../../domain/electron/electronFuses.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import { err, ok, type Result } from "../../domain/result.js";
const OPERATION = "inspect_electron_fuses";
/** Shared exact input validation and artifact-bound Evidence for CLI and MCP. */
export class ElectronFuseService {
constructor(readonly provider: ElectronFusePort) {}
/** Read fuse wires for one explicitly selected path without launching it. */
async inspect(
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(OPERATION));
const input = inspectElectronFusesInputSchema.safeParse(rawInput);
if (!input.success)
return err(
analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, {
cause: input.error,
}),
);
if (!isAbsolute(input.data.path))
return err(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["path"],
reason: "invalid_format",
message: "Expected an absolute filesystem path on this host.",
},
]),
);
const inspected = await this.provider.inspect(input.data, options);
if (!inspected.ok) return inspected;
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(OPERATION));
const report = electronFuseReportSchema.safeParse(inspected.value);
if (
!report.success ||
report.data.artifact.selected_path !== input.data.path
)
return err(
new AnalysisOutputError(
OPERATION,
"Electron fuse reader returned a malformed report or changed the selected artifact identity.",
),
);
const value = report.data;
const architecture = subjectArchitecture(value);
return ok(
createEvidence(
{
path: value.artifact.resolved_path,
sha256: value.artifact.sha256,
format: value.artifact.format,
...(architecture === undefined ? {} : { architecture }),
},
this.provider.identity,
{
operation: OPERATION,
parameters: input.data,
result: value,
confidence: "observed",
limitations: value.limitations,
locations: locationsFor(value),
},
),
);
}
}
const subjectArchitecture = (
report: ReturnType<typeof electronFuseReportSchema.parse>,
) => {
if (report.artifact.format === "mach-o-universal") return undefined;
if (report.slices.length !== 1) return undefined;
const name = report.slices[0]?.architecture;
return BINARY_ARCHITECTURES.find((supported) => supported === name);
};
const locationsFor = (
report: ReturnType<typeof electronFuseReportSchema.parse>,
) => [
{ kind: "artifact-path" as const, path: report.artifact.selected_path },
...(report.artifact.resolved_path === report.artifact.selected_path
? []
: [
{
kind: "artifact-path" as const,
path: report.artifact.resolved_path,
},
]),
...report.slices.flatMap((slice) =>
slice.sentinels.map((sentinel) => ({
kind: "file-offset" as const,
offset: sentinel.sentinel_offset,
})),
),
];

View File

@ -1,3 +1,5 @@
import { resolve } from "node:path";
import { Cli, z } from "incur";
import { logCliCommand } from "../cliLogging.js";
@ -12,6 +14,7 @@ import { projectInputIssues } from "../domain/inputIssueProjection.js";
import type { JsonValue } from "../domain/jsonValue.js";
import type { Logger } from "pino";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { createElectronFuseService } from "../composition/electronFuses.js";
import { parseCliJsonInput, resolveCliJsonPaths } from "../cliJsonInput.js";
import {
electronPageInspectionOptions,
@ -27,11 +30,40 @@ export const registerElectronCommands = (
environment: Readonly<Record<string, string | undefined>>,
): void => {
registerElectronObservationCommands(cli, logger);
registerElectronFuseCommand(cli, logger);
registerElectronActiveCommand(cli, logger, environment);
registerJavaScriptApplicationCommand(cli, logger);
registerJavaScriptRuntimeReconciliationCommand(cli, logger);
};
const registerElectronFuseCommand = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
): void => {
const service = createElectronFuseService();
cli.command(CLI_COMMANDS.inspectElectronFuses, {
description:
"Inspect packaged Electron fuse bytes without launching the application",
args: z.object({
path: z
.string()
.describe(
"Local Electron .app, Electron Framework binary, PE executable, or ELF executable",
),
}),
run: ({ args }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, CLI_COMMANDS.inspectElectronFuses, async () => {
const result = await service.inspect(
{ path: resolve(args.path) },
{ signal },
);
return result.ok ? result.value : projectAnalysisError(result.error);
}),
),
});
};
const registerElectronActiveCommand = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,

View File

@ -138,6 +138,7 @@ export const CLI_COMMANDS = Object.freeze({
captureBrowserScenario: "capture-browser-scenario",
listElectronTargets: "list-electron-targets",
inspectElectronPage: "inspect-electron-page",
inspectElectronFuses: "inspect-electron-fuses",
captureElectronScenario: "capture-electron-scenario",
listJavaScriptRuntimeTargets: "list-javascript-runtime-targets",
observeJavaScriptRuntime: "observe-javascript-runtime",

View File

@ -0,0 +1,6 @@
import { ElectronFuseService } from "../application/electron/ElectronFuseService.js";
import { ElectronFuseProvider } from "../electron/ElectronFuseProvider.js";
/** Compose read-only fuse inspection without a runtime, network, or fuse edit. */
export const createElectronFuseService = (): ElectronFuseService =>
new ElectronFuseService(new ElectronFuseProvider());

View File

@ -1,6 +1,8 @@
import type { ToolContract } from "../toolContractTypes.js";
import { toolContractMetadata } from "../toolEffects.js";
import { electronFuseOutputSchemas } from "../toolOutputSchemaGroups.js";
import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js";
import { inspectElectronFusesInputSchema } from "../../domain/electron/electronFuses.js";
import {
electronPageInspectionSchema,
electronTargetListSchema,
@ -147,4 +149,19 @@ export const ELECTRON_TOOL_CONTRACTS = [
{ title: "Exercise an owned Electron application", input: activeExample },
],
},
{
name: "inspect_electron_fuses",
...toolContractMetadata("inspect_electron_fuses"),
kind: "artifact-provider",
description:
"Inspect packaged Electron fuse bytes in one local macOS app bundle, Electron Framework binary, Windows PE executable, or Linux ELF executable. Returns the artifact digest, per-slice wire version, fuse index, version-1 name, state, and raw byte, plus derived implications for Inspector flags, ELECTRON_RUN_AS_NODE, embedded ASAR integrity, and app.asar-only loading. A missing sentinel is unknown fuse state. Reads artifact bytes only and does not modify the selected file or open a runtime target.",
inputSchema: inspectElectronFusesInputSchema,
outputSchema: electronFuseOutputSchemas.inspect_electron_fuses,
examples: [
{
title: "Read packaged Electron fuse state",
input: { path: "/Applications/Example.app" },
},
],
},
] as const satisfies readonly ToolContract[];

View File

@ -648,6 +648,7 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
list_javascript_runtime_targets: browserEvidence,
observe_javascript_runtime: browserEvidence,
analyze_javascript_application: evidence,
inspect_electron_fuses: evidence,
export_web_scripts: effects({
mutatesSession: true,
writesFilesystem: true,

View File

@ -88,6 +88,7 @@ import {
} from "./toolOutputSchemaPrimitives.js";
import { processCaptureSchema } from "../domain/process/processCapture.js";
import { electronFuseReportSchema } from "../domain/electron/electronFuses.js";
const contextFacetSchema = z.discriminatedUnion("state", [
z.object({ state: z.literal("available"), value: jsonValueSchema }),
z.object({
@ -584,3 +585,8 @@ export const sessionOutputSchemas = {
}),
),
} satisfies Readonly<Record<string, z.ZodObject>>;
/** Exact Evidence schema for read-only Electron fuse inspection. */
export const electronFuseOutputSchemas = {
inspect_electron_fuses: evidenceResultOf(electronFuseReportSchema),
};

View File

@ -0,0 +1,296 @@
import { describe, expect, it } from "vitest";
import {
describeElectronFuseFindings,
electronFuseName,
electronFuseReportSchema,
electronFuseState,
ELECTRON_FUSE_SENTINEL,
ELECTRON_FUSE_STATE_BYTES,
ELECTRON_FUSE_V1_NAMES,
type ElectronFuseSlice,
} from "./electronFuses.js";
const artifact = {
selected_path: "/artifacts/Electron",
resolved_path: "/artifacts/Electron",
sha256: "a".repeat(64),
bytes: 128,
format: "mach-o" as const,
};
const sentinel = (
offset: number,
fuses: readonly number[],
options?: {
readonly version?: number | null;
readonly declared?: number | null;
readonly complete?: boolean;
},
) => ({
sentinel_offset: offset,
wire_version: options?.version === undefined ? 1 : options.version,
declared_length:
options?.declared === undefined ? fuses.length : options.declared,
fuses: fuses.map((raw, index) => ({
index,
name: electronFuseName(
options?.version === undefined ? 1 : (options.version ?? 1),
index,
),
state: electronFuseState(raw),
raw_byte: raw,
})),
complete: options?.complete ?? true,
});
const slice = (
sentinels: ReturnType<typeof sentinel>[],
index = 0,
): ElectronFuseSlice => ({
index,
file_offset: 0,
size: 128,
architecture: "arm64",
sentinel_count: sentinels.length,
sentinels,
});
it("validates implications across repeated FAT slices and rejects a false claim", () => {
const slices = Array.from({ length: 128 }, (_, index) => ({
...slice(
Array.from({ length: 32 }, (_, offset) =>
sentinel(
offset * 64,
Array.from({ length: 8 }, () => ELECTRON_FUSE_STATE_BYTES.enabled),
),
),
index,
),
size: 4096,
}));
const report = {
artifact: { ...artifact, bytes: 4096, format: "mach-o-universal" as const },
slices,
...describeElectronFuseFindings(slices, "mach-o-universal"),
};
expect(electronFuseReportSchema.safeParse(report).success).toBe(true);
const last = report.runtime_implications.at(-1);
expect(last).toBeDefined();
expect(
electronFuseReportSchema.safeParse({
...report,
runtime_implications: [{ ...last, outcome: "unknown" }],
}).success,
).toBe(false);
});
describe("Electron fuse wire decoding", () => {
it("keeps the 32-byte sentinel and version-1 names pinned", () => {
expect(ELECTRON_FUSE_SENTINEL).toHaveLength(32);
expect(ELECTRON_FUSE_V1_NAMES[3]).toBe("EnableNodeCliInspectArguments");
expect(electronFuseName(1, 8)).toBe("WasmTrapHandlers");
expect(electronFuseName(1, 9)).toBeNull();
expect(electronFuseName(2, 0)).toBeNull();
});
it("preserves disabled, enabled, removed, and unknown bytes", () => {
expect(electronFuseState(ELECTRON_FUSE_STATE_BYTES.disabled)).toBe(
"disabled",
);
expect(electronFuseState(ELECTRON_FUSE_STATE_BYTES.enabled)).toBe(
"enabled",
);
expect(electronFuseState(ELECTRON_FUSE_STATE_BYTES.removed)).toBe(
"removed",
);
expect(electronFuseState(0x99)).toBe("unknown");
});
it("derives runtime implications from the fuse index and leaves unknowns unknown", () => {
const fuses = [
ELECTRON_FUSE_STATE_BYTES.disabled,
ELECTRON_FUSE_STATE_BYTES.enabled,
0x99,
ELECTRON_FUSE_STATE_BYTES.removed,
ELECTRON_FUSE_STATE_BYTES.disabled,
ELECTRON_FUSE_STATE_BYTES.enabled,
];
const report = electronFuseReportSchema.parse({
artifact,
slices: [slice([sentinel(8, fuses)])],
...describeElectronFuseFindings([slice([sentinel(8, fuses)])], "mach-o"),
});
expect(report.runtime_implications).toEqual([
expect.objectContaining({
subject: "node_cli_inspect_arguments",
fuse_index: 3,
outcome: "not_honored",
relationship: "derivation",
}),
expect.objectContaining({
subject: "electron_run_as_node",
fuse_index: 0,
outcome: "not_honored",
}),
expect.objectContaining({
subject: "embedded_asar_integrity_validation",
fuse_index: 4,
outcome: "not_enforced",
}),
expect.objectContaining({
subject: "only_load_app_from_asar",
fuse_index: 5,
outcome: "restricted_to_app_asar",
}),
]);
expect(report.slices[0] !== undefined && "fuses" in report.slices[0]).toBe(
false,
);
expect(report.slices[0]?.sentinels[0]?.fuses[2]).toMatchObject({
index: 2,
name: "EnableNodeOptionsEnvironmentVariable",
state: "unknown",
raw_byte: 0x99,
});
});
it("does not treat a missing sentinel or a short wire as disabled fuses", () => {
const missing = describeElectronFuseFindings([slice([])], "mach-o");
expect(missing.runtime_implications).toEqual([]);
expect(missing.limitations.join(" ")).toContain(
"unknown, not that every fuse is disabled",
);
expect(missing.limitations.join(" ")).toContain(
"fuse state for that slice is unknown, not disabled",
);
const truncated = describeElectronFuseFindings(
[
slice([
sentinel(8, [ELECTRON_FUSE_STATE_BYTES.enabled], {
declared: 4,
complete: false,
}),
]),
],
"mach-o",
);
expect(truncated.runtime_implications).toEqual([
expect.objectContaining({
subject: "electron_run_as_node",
outcome: "honored",
}),
]);
expect(truncated.limitations.join(" ")).toContain("truncated");
expect(truncated.limitations.join(" ")).toContain("fuse index 3");
});
it("reports every sentinel and skips implications for an unpinned wire version", () => {
const findings = describeElectronFuseFindings(
[
slice([
sentinel(8, [ELECTRON_FUSE_STATE_BYTES.enabled], { version: 2 }),
sentinel(
40,
Array.from({ length: 6 }, () => ELECTRON_FUSE_STATE_BYTES.disabled),
),
]),
],
"mach-o",
);
expect(findings.runtime_implications).toHaveLength(4);
expect(findings.limitations.join(" ")).toContain("none is selected");
expect(findings.limitations.join(" ")).toContain("wire version 2");
expect(
findings.runtime_implications.every(
(item) => item.sentinel_offset === 40,
),
).toBe(true);
});
});
describe("ELF embedded ASAR integrity applicability", () => {
it("does not claim ASAR integrity enforcement for an enabled fuse on ELF", () => {
const fuses = [
ELECTRON_FUSE_STATE_BYTES.disabled,
ELECTRON_FUSE_STATE_BYTES.disabled,
ELECTRON_FUSE_STATE_BYTES.disabled,
ELECTRON_FUSE_STATE_BYTES.disabled,
ELECTRON_FUSE_STATE_BYTES.enabled,
];
const slices = [slice([sentinel(4, fuses)])];
const elf = describeElectronFuseFindings(slices, "elf");
expect(slices[0]?.sentinels[0]?.fuses[4]).toMatchObject({
index: 4,
name: "EnableEmbeddedAsarIntegrityValidation",
state: "enabled",
raw_byte: ELECTRON_FUSE_STATE_BYTES.enabled,
});
expect(
elf.runtime_implications.find(
(item) => item.subject === "embedded_asar_integrity_validation",
),
).toMatchObject({
relationship: "derivation",
fuse_index: 4,
outcome: "unknown",
});
expect(elf.limitations.join(" ")).toContain("only on macOS and Windows");
expect(
describeElectronFuseFindings(slices, "mach-o").runtime_implications.find(
(item) => item.subject === "embedded_asar_integrity_validation",
),
).toMatchObject({ outcome: "enforced" });
expect(
describeElectronFuseFindings(slices, "pe").runtime_implications.find(
(item) => item.subject === "embedded_asar_integrity_validation",
),
).toMatchObject({ outcome: "enforced" });
expect(
describeElectronFuseFindings(
slices,
"mach-o-universal",
).runtime_implications.find(
(item) => item.subject === "embedded_asar_integrity_validation",
),
).toMatchObject({ outcome: "enforced" });
});
});
it("rejects claimed wire bytes outside their slice and unsupported derivations", () => {
const slices = [slice([sentinel(4, [ELECTRON_FUSE_STATE_BYTES.disabled])])];
const findings = describeElectronFuseFindings(slices, "mach-o");
const report = { artifact, slices, ...findings };
expect(electronFuseReportSchema.safeParse(report).success).toBe(true);
expect(
electronFuseReportSchema.safeParse({
...report,
artifact: { ...artifact, bytes: 36 },
slices: [{ ...slices[0], size: 36 }],
}).success,
).toBe(false);
expect(
electronFuseReportSchema.safeParse({
...report,
runtime_implications: findings.runtime_implications.map((fact) => ({
...fact,
outcome: "honored",
})),
}).success,
).toBe(false);
expect(
electronFuseReportSchema.safeParse({
...report,
runtime_implications: [
{
relationship: "derivation",
subject: "node_cli_inspect_arguments",
fuse_index: 3,
outcome: "honored",
slice_index: 0,
sentinel_offset: 4,
},
],
}).success,
).toBe(false);
});

View File

@ -0,0 +1,477 @@
import { z } from "zod";
import { digestSchema } from "../digests.js";
import { localPathStringSchema } from "../localPath.js";
/** ASCII sentinel that introduces an Electron fuse wire in an Electron binary. */
export const ELECTRON_FUSE_SENTINEL = "dL7pKGdnNz796PbbjQWNKmHXBZaB9tsX";
/** Wire bytes from the Electron fuse wire: disabled, enabled, and removed. */
export const ELECTRON_FUSE_STATE_BYTES = {
disabled: 0x30,
enabled: 0x31,
removed: 0x72,
} as const;
/**
* FuseV1 index order. An index past this table, or any index on another wire
* version, keeps a null name and its raw byte.
*/
export const ELECTRON_FUSE_V1_NAMES = [
"RunAsNode",
"EnableCookieEncryption",
"EnableNodeOptionsEnvironmentVariable",
"EnableNodeCliInspectArguments",
"EnableEmbeddedAsarIntegrityValidation",
"OnlyLoadAppFromAsar",
"LoadBrowserProcessSpecificV8Snapshot",
"GrantFileProtocolExtraPrivileges",
"WasmTrapHandlers",
] as const;
const FUSE_IMPLICATIONS = [
{
subject: "node_cli_inspect_arguments",
fuseIndex: 3,
enabled: "honored",
inactive: "not_honored",
},
{
subject: "electron_run_as_node",
fuseIndex: 0,
enabled: "honored",
inactive: "not_honored",
},
{
subject: "embedded_asar_integrity_validation",
fuseIndex: 4,
enabled: "enforced",
inactive: "not_enforced",
},
{
subject: "only_load_app_from_asar",
fuseIndex: 5,
enabled: "restricted_to_app_asar",
inactive: "not_restricted_to_app_asar",
},
] as const;
const OTHER_MEANS_LIMITATION =
"Fuse state does not prove the application enables Inspector through other means, such as app.commandLine or a remote-debugging switch in application code.";
const MISSING_SENTINEL_LIMITATION =
"A missing sentinel means fuse state is unknown, not that every fuse is disabled.";
const byteSchema = z.number().int().min(0).max(255);
const fuseStateSchema = z.enum(["enabled", "disabled", "removed", "unknown"]);
/** One fuse byte, with a version-1 name when the pinned table covers its index. */
export const electronFuseSchema = z.strictObject({
index: z.number().int().nonnegative(),
name: z.string().min(1).nullable(),
state: fuseStateSchema,
raw_byte: byteSchema,
});
const electronFuseSentinelSchema = z.strictObject({
sentinel_offset: z.number().int().nonnegative(),
wire_version: byteSchema.nullable(),
declared_length: byteSchema.nullable(),
fuses: z.array(electronFuseSchema),
complete: z.boolean(),
});
const electronFuseSliceSchema = z.strictObject({
index: z.number().int().nonnegative(),
file_offset: z.number().int().nonnegative(),
size: z.number().int().nonnegative(),
architecture: z.string().min(1).nullable(),
sentinel_count: z.number().int().nonnegative(),
sentinels: z.array(electronFuseSentinelSchema),
});
const implicationLocation = {
relationship: z.literal("derivation"),
slice_index: z.number().int().nonnegative(),
sentinel_offset: z.number().int().nonnegative(),
};
/** Derived runtime meaning of one observed fuse byte. The fuse bytes stay observed. */
export const electronFuseImplicationSchema = z.discriminatedUnion("subject", [
z.strictObject({
...implicationLocation,
subject: z.literal("node_cli_inspect_arguments"),
fuse_index: z.literal(3),
outcome: z.enum(["honored", "not_honored", "unknown"]),
}),
z.strictObject({
...implicationLocation,
subject: z.literal("electron_run_as_node"),
fuse_index: z.literal(0),
outcome: z.enum(["honored", "not_honored", "unknown"]),
}),
z.strictObject({
...implicationLocation,
subject: z.literal("embedded_asar_integrity_validation"),
fuse_index: z.literal(4),
outcome: z.enum(["enforced", "not_enforced", "unknown"]),
}),
z.strictObject({
...implicationLocation,
subject: z.literal("only_load_app_from_asar"),
fuse_index: z.literal(5),
outcome: z.enum([
"restricted_to_app_asar",
"not_restricted_to_app_asar",
"unknown",
]),
}),
]);
/** Inline fuse observation for one selected app bundle or Electron executable. */
export const electronFuseReportSchema = z
.strictObject({
artifact: z
.strictObject({
selected_path: localPathStringSchema,
resolved_path: localPathStringSchema,
sha256: digestSchema,
bytes: z.number().int().nonnegative(),
format: z.enum(["mach-o", "mach-o-universal", "pe", "elf"]),
})
.describe(
"Caller-selected path, resolved regular file, and the container that was read",
),
slices: z
.array(electronFuseSliceSchema)
.describe(
"One entry per universal Mach-O slice, or a single entry for a thin Mach-O, PE, or ELF file. Sentinel offsets are file-absolute.",
),
runtime_implications: z
.array(electronFuseImplicationSchema)
.describe(
"Derivations from version-1 fuse bytes. Each entry names the fuse index it came from.",
),
limitations: z.array(z.string().min(1)),
})
.superRefine((report, context) => {
validateReportBounds(report, context);
});
export const inspectElectronFusesInputSchema = z.strictObject({
path: localPathStringSchema.describe(
"Absolute filesystem path to a macOS .app bundle, Electron Framework binary, Windows PE executable, or Linux ELF executable",
),
});
export type ElectronFuseState = z.infer<typeof fuseStateSchema>;
export type ElectronFuseSentinel = z.infer<typeof electronFuseSentinelSchema>;
export type ElectronFuseSlice = z.infer<typeof electronFuseSliceSchema>;
export type ElectronFuseImplication = z.infer<
typeof electronFuseImplicationSchema
>;
export type ElectronFuseReport = z.infer<typeof electronFuseReportSchema>;
export type InspectElectronFusesInput = z.infer<
typeof inspectElectronFusesInputSchema
>;
/** Decode one fuse wire byte. Any other value stays unknown and keeps the raw byte. */
export const electronFuseState = (byte: number): ElectronFuseState => {
if (byte === ELECTRON_FUSE_STATE_BYTES.disabled) return "disabled";
if (byte === ELECTRON_FUSE_STATE_BYTES.enabled) return "enabled";
if (byte === ELECTRON_FUSE_STATE_BYTES.removed) return "removed";
return "unknown";
};
/** Pinned version-1 name, or null when this wire version or index has no table entry. */
export const electronFuseName = (
wireVersion: number,
index: number,
): string | null =>
wireVersion === 1 ? (ELECTRON_FUSE_V1_NAMES[index] ?? null) : null;
/**
* Attach derivations and reading limitations without choosing among multiple
* sentinels. Embedded ASAR integrity is compiled only for Mach-O and PE, so an
* ELF byte stays observed and its enforcement outcome stays unknown.
*/
export const describeElectronFuseFindings = (
slices: readonly ElectronFuseSlice[],
container: ElectronFuseReport["artifact"]["format"],
): {
readonly runtime_implications: ElectronFuseImplication[];
readonly limitations: string[];
} => {
const runtime_implications: ElectronFuseImplication[] = [];
const limitations = [OTHER_MEANS_LIMITATION, MISSING_SENTINEL_LIMITATION];
for (const slice of slices) {
if (slice.sentinel_count === 0)
limitations.push(
`Slice ${slice.index} at file offset ${slice.file_offset} contains no Electron fuse sentinel; fuse state for that slice is unknown, not disabled.`,
);
else if (slice.sentinel_count > 1)
limitations.push(
`Slice ${slice.index} at file offset ${slice.file_offset} contains ${slice.sentinel_count} Electron fuse sentinels; every sentinel is reported and none is selected.`,
);
for (const sentinel of slice.sentinels)
describeSentinel(
slice.index,
sentinel,
container,
runtime_implications,
limitations,
);
}
return { runtime_implications, limitations };
};
/** Electron compiles this validation for macOS (Mach-O) and Windows (PE) only. */
const embeddedAsarIntegrityApplies = (
container: ElectronFuseReport["artifact"]["format"],
): boolean =>
container === "mach-o" ||
container === "mach-o-universal" ||
container === "pe";
const describeSentinel = (
sliceIndex: number,
sentinel: ElectronFuseSentinel,
container: ElectronFuseReport["artifact"]["format"],
implications: ElectronFuseImplication[],
limitations: string[],
): void => {
if (!sentinel.complete)
limitations.push(
`Electron fuse wire at file offset ${sentinel.sentinel_offset} is truncated; fuse bytes past the slice stay unknown.`,
);
if (sentinel.wire_version === null) return;
if (sentinel.wire_version !== 1) {
limitations.push(
`Electron fuse wire at file offset ${sentinel.sentinel_offset} uses wire version ${sentinel.wire_version}; version 1 is the only pinned name table, so runtime implications for this wire are unknown.`,
);
return;
}
for (const spec of FUSE_IMPLICATIONS) {
const fuse = sentinel.fuses.find((item) => item.index === spec.fuseIndex);
if (fuse === undefined) {
limitations.push(
`Electron fuse wire at file offset ${sentinel.sentinel_offset} does not include fuse index ${spec.fuseIndex}; that runtime implication is unknown.`,
);
continue;
}
const applicabilityUnknown =
spec.subject === "embedded_asar_integrity_validation" &&
!embeddedAsarIntegrityApplies(container);
if (applicabilityUnknown)
limitations.push(
`Electron compiles embedded ASAR integrity validation only on macOS and Windows; the observed fuse byte at file offset ${sentinel.sentinel_offset} does not establish enforcement.`,
);
implications.push(
implicationFor(spec, applicabilityUnknown ? "unknown" : fuse.state, {
slice_index: sliceIndex,
sentinel_offset: sentinel.sentinel_offset,
}),
);
}
};
const implicationFor = (
spec: (typeof FUSE_IMPLICATIONS)[number],
state: ElectronFuseState,
location: {
readonly slice_index: number;
readonly sentinel_offset: number;
},
): ElectronFuseImplication => {
const outcome =
state === "enabled"
? spec.enabled
: state === "unknown"
? "unknown"
: spec.inactive;
return {
relationship: "derivation",
subject: spec.subject,
fuse_index: spec.fuseIndex,
outcome,
...location,
} as ElectronFuseImplication;
};
const validateReportBounds = (
report: ElectronFuseReport,
context: z.RefinementCtx,
): void => {
const size = report.artifact.bytes;
for (const [sliceIndex, slice] of report.slices.entries()) {
if (slice.index !== sliceIndex)
context.addIssue({
code: "custom",
path: ["slices", sliceIndex, "index"],
message: "Slice index does not match its position in the report.",
});
if (slice.sentinel_count !== slice.sentinels.length)
context.addIssue({
code: "custom",
path: ["slices", sliceIndex, "sentinel_count"],
message: "Sentinel count does not match the reported sentinels.",
});
if (slice.file_offset > size || slice.size > size - slice.file_offset)
context.addIssue({
code: "custom",
path: ["slices", sliceIndex],
message: "Slice range extends outside the selected artifact.",
});
for (const [sentinelIndex, sentinel] of slice.sentinels.entries())
validateSentinel(slice, sentinel, sliceIndex, sentinelIndex, context);
}
const expected = new Set(
describeElectronFuseFindings(
report.slices,
report.artifact.format,
).runtime_implications.map(implicationKey),
);
for (const [index, implication] of report.runtime_implications.entries()) {
if (!expected.has(implicationKey(implication)))
context.addIssue({
code: "custom",
path: ["runtime_implications", index],
message:
"Runtime implication is not supported by the reported fuse bytes and applicability.",
});
const slice = report.slices[implication.slice_index];
if (
slice?.sentinels.some(
(sentinel) => sentinel.sentinel_offset === implication.sentinel_offset,
) !== true
)
context.addIssue({
code: "custom",
path: ["runtime_implications", index],
message: "Runtime implication does not reference a reported sentinel.",
});
}
};
const implicationKey = (fact: ElectronFuseImplication): string =>
JSON.stringify([
fact.slice_index,
fact.sentinel_offset,
fact.subject,
fact.fuse_index,
fact.outcome,
]);
const validateSentinel = (
slice: ElectronFuseSlice,
sentinel: ElectronFuseSentinel,
sliceIndex: number,
sentinelIndex: number,
context: z.RefinementCtx,
): void => {
const sentinelEnd = sentinel.sentinel_offset + ELECTRON_FUSE_SENTINEL.length;
const wireEnd =
sentinelEnd +
(sentinel.wire_version === null ? 0 : 1) +
(sentinel.declared_length === null ? 0 : 1) +
sentinel.fuses.length;
if (wireEnd > slice.file_offset + slice.size)
context.addIssue({
code: "custom",
path: ["slices", sliceIndex, "sentinels", sentinelIndex],
message: "Reported wire header or fuse bytes extend outside the slice.",
});
if (
sentinel.sentinel_offset < slice.file_offset ||
sentinelEnd > slice.file_offset + slice.size
)
context.addIssue({
code: "custom",
path: [
"slices",
sliceIndex,
"sentinels",
sentinelIndex,
"sentinel_offset",
],
message: "Sentinel is outside its slice.",
});
if (sentinel.wire_version === null && sentinel.declared_length !== null)
context.addIssue({
code: "custom",
path: [
"slices",
sliceIndex,
"sentinels",
sentinelIndex,
"declared_length",
],
message: "A wire without a version byte cannot declare a fuse count.",
});
if (
sentinel.complete &&
(sentinel.declared_length === null ||
sentinel.fuses.length !== sentinel.declared_length)
)
context.addIssue({
code: "custom",
path: ["slices", sliceIndex, "sentinels", sentinelIndex, "complete"],
message: "A complete wire must contain every declared fuse byte.",
});
if (
!sentinel.complete &&
sentinel.declared_length !== null &&
sentinel.fuses.length >= sentinel.declared_length
)
context.addIssue({
code: "custom",
path: ["slices", sliceIndex, "sentinels", sentinelIndex, "complete"],
message: "An incomplete wire must omit at least one declared fuse byte.",
});
for (const [fuseIndex, fuse] of sentinel.fuses.entries()) {
if (fuse.index !== fuseIndex)
context.addIssue({
code: "custom",
path: [
"slices",
sliceIndex,
"sentinels",
sentinelIndex,
"fuses",
fuseIndex,
"index",
],
message: "Fuse index does not match its wire position.",
});
if (fuse.state !== electronFuseState(fuse.raw_byte))
context.addIssue({
code: "custom",
path: [
"slices",
sliceIndex,
"sentinels",
sentinelIndex,
"fuses",
fuseIndex,
"state",
],
message: "Fuse state does not match its raw byte.",
});
if (
sentinel.wire_version !== null &&
fuse.name !== electronFuseName(sentinel.wire_version, fuse.index)
)
context.addIssue({
code: "custom",
path: [
"slices",
sliceIndex,
"sentinels",
sentinelIndex,
"fuses",
fuseIndex,
"name",
],
message: "Fuse name does not match the pinned version-1 table.",
});
}
};

View File

@ -0,0 +1,307 @@
import type { BigIntStats } from "node:fs";
import { lstat, readlink } from "node:fs/promises";
import { basename, dirname, isAbsolute, join, sep } from "node:path";
import { isPathWithinRoot } from "../domain/localPath.js";
const FRAMEWORK_NAME = "Electron Framework.framework";
const BINARY_NAME = "Electron Framework";
const MAX_SYMLINK_HOPS = 32;
/** Filesystem facts owned by path admission and consumed unchanged by the reader. */
export interface ElectronFusePathNode {
readonly path: string;
readonly metadata: BigIntStats;
}
export interface ResolvedElectronFuseFile {
readonly path: string;
readonly metadata: BigIntStats;
readonly nodes: readonly ElectronFusePathNode[];
}
export interface ResolvedElectronFuseTarget {
readonly selectedPath: string;
readonly file: ResolvedElectronFuseFile;
}
/** A selected path is not an admitted Electron bundle or regular file. */
export class ElectronFusePathFailure extends Error {
constructor(
readonly reason: "invalid_value" | "invalid_format",
message: string,
) {
super(message);
this.name = "ElectronFusePathFailure";
}
}
type WalkResult =
| ({ readonly kind: "file" } & ResolvedElectronFuseFile)
| { readonly kind: "absent" | "escapes" | "not-file" | "loop" };
/**
* Admit a regular executable, a macOS app bundle, or an Electron Framework
* binary reached through `Versions/Current` without leaving the framework.
*/
export const resolveElectronFuseTarget = async (
selectedPath: string,
signal?: AbortSignal,
): Promise<ResolvedElectronFuseTarget> => {
signal?.throwIfAborted();
const metadata = await lstat(selectedPath, { bigint: true });
let file: ResolvedElectronFuseFile;
if (metadata.isSymbolicLink())
file = await resolveFrameworkSymlink(selectedPath, signal);
else if (metadata.isFile())
file =
frameworkRootOf(selectedPath) === null
? { path: selectedPath, metadata, nodes: [] }
: await resolveFrameworkSymlink(selectedPath, signal);
else if (metadata.isDirectory())
file = await resolveBundleBinary(selectedPath, signal);
else
throw new ElectronFusePathFailure(
"invalid_format",
`Expected a regular file or Electron app bundle: ${selectedPath}`,
);
return { selectedPath, file };
};
const resolveFrameworkSymlink = async (
selectedPath: string,
signal?: AbortSignal,
): Promise<ResolvedElectronFuseFile> => {
const root = frameworkRootOf(selectedPath);
if (root === null)
throw new ElectronFusePathFailure(
"invalid_format",
`Expected a regular file without a symlink: ${selectedPath}`,
);
const nodes = await requireFrameworkRoot(root, selectedPath);
const relativePath = relativeInside(root, selectedPath);
if (relativePath === null)
throw new ElectronFusePathFailure(
"invalid_value",
`Framework symlink escapes ${FRAMEWORK_NAME}: ${selectedPath}`,
);
return requireFile(
await resolveInside(root, relativePath, signal),
selectedPath,
nodes,
);
};
const resolveBundleBinary = async (
selectedPath: string,
signal?: AbortSignal,
): Promise<ResolvedElectronFuseFile> => {
const name = basename(selectedPath);
if (name === FRAMEWORK_NAME)
return resolveKnownBinary(selectedPath, selectedPath, signal);
if (name.endsWith(".app")) {
return resolveKnownBinary(
join(selectedPath, "Contents", "Frameworks", FRAMEWORK_NAME),
selectedPath,
signal,
);
}
throw new ElectronFusePathFailure(
"invalid_format",
`Expected an Electron .app bundle, ${FRAMEWORK_NAME} directory, or executable file: ${selectedPath}`,
);
};
/** `root` is the framework directory for both an `.app` and a framework selection. */
const resolveKnownBinary = async (
root: string,
bundle: string,
signal: AbortSignal | undefined,
): Promise<ResolvedElectronFuseFile> => {
const nodes = await requireFrameworkRoot(root, bundle);
for (const candidate of [
BINARY_NAME,
`Versions/Current/${BINARY_NAME}`,
`Versions/A/${BINARY_NAME}`,
]) {
const selected = await resolveInside(root, candidate, signal);
if (selected.kind === "file") return requireFile(selected, bundle, nodes);
if (selected.kind !== "absent")
throw pathWalkFailure(selected.kind, bundle);
}
throw new ElectronFusePathFailure(
"invalid_value",
`Electron Framework binary was not found inside ${bundle}.`,
);
};
const resolveInside = async (
root: string,
relativePath: string,
signal?: AbortSignal,
): Promise<WalkResult> => {
const pending = relativePath.split("/").filter((segment) => segment !== "");
const resolved: string[] = [];
const nodes: ElectronFusePathNode[] = [];
let fileMetadata: BigIntStats | undefined;
let hops = 0;
let last: "file" | "directory" | undefined;
while (pending.length > 0) {
signal?.throwIfAborted();
const segment = pending.shift() ?? "";
if (segment === ".") continue;
if (segment === "..") {
if (resolved.length === 0) return { kind: "escapes" };
resolved.pop();
last = "directory";
continue;
}
const absolute = join(root, ...resolved, segment);
if (!isPathWithinRoot(root, absolute)) return { kind: "escapes" };
let metadata;
try {
metadata = await lstat(absolute, { bigint: true });
} catch (cause: unknown) {
if (isMissing(cause)) return { kind: "absent" };
throw cause;
}
if (metadata.isSymbolicLink()) {
nodes.push({ path: absolute, metadata });
hops += 1;
if (hops > MAX_SYMLINK_HOPS) return { kind: "loop" };
const target = await readlink(absolute);
if (isAbsolute(target)) return { kind: "escapes" };
pending.unshift(...target.split("/").filter((part) => part !== ""));
continue;
}
if (metadata.isDirectory()) {
nodes.push({ path: absolute, metadata });
resolved.push(segment);
last = "directory";
continue;
}
if (!metadata.isFile() || pending.length > 0) return { kind: "not-file" };
resolved.push(segment);
fileMetadata = metadata;
last = "file";
}
if (last !== "file" || fileMetadata === undefined)
return { kind: "not-file" };
const path = join(root, ...resolved);
return isPathWithinRoot(root, path)
? { kind: "file", path, metadata: fileMetadata, nodes }
: { kind: "escapes" };
};
/** Validate the app-parent chain equally for app, framework, and binary selection. */
const requireFrameworkRoot = async (
root: string,
selectedPath: string,
): Promise<readonly ElectronFusePathNode[]> => {
const frameworks = dirname(root);
const contents = dirname(frameworks);
const app = dirname(contents);
if (
basename(frameworks) === "Frameworks" &&
basename(contents) === "Contents" &&
basename(app).endsWith(".app")
)
return requireRealDirectories(
dirname(app),
[basename(app), "Contents", "Frameworks", FRAMEWORK_NAME],
selectedPath,
);
else
return requireRealDirectories(
dirname(root),
[basename(root)],
selectedPath,
);
};
/**
* Each named directory below `anchor` must be a real directory, so the
* framework root cannot redirect the walk outside the selected bundle.
*/
const requireRealDirectories = async (
anchor: string,
segments: readonly string[],
selectedPath: string,
): Promise<readonly ElectronFusePathNode[]> => {
let current = anchor;
const nodes: ElectronFusePathNode[] = [];
for (const segment of segments) {
current = join(current, segment);
let metadata;
try {
metadata = await lstat(current, { bigint: true });
} catch (cause: unknown) {
if (isMissing(cause))
throw new ElectronFusePathFailure(
"invalid_value",
`Electron Framework binary was not found inside ${selectedPath}.`,
);
throw cause;
}
nodes.push({ path: current, metadata });
if (metadata.isSymbolicLink())
throw pathWalkFailure("escapes", selectedPath);
if (!metadata.isDirectory())
throw pathWalkFailure("not-file", selectedPath);
}
return nodes;
};
const requireFile = (
walk: WalkResult,
selectedPath: string,
nodes: readonly ElectronFusePathNode[],
): ResolvedElectronFuseFile => {
if (walk.kind === "file")
return {
path: walk.path,
metadata: walk.metadata,
nodes: [...nodes, ...walk.nodes],
};
if (walk.kind === "absent")
throw new ElectronFusePathFailure(
"invalid_value",
`Electron Framework symlink target was not found: ${selectedPath}`,
);
throw pathWalkFailure(walk.kind, selectedPath);
};
const pathWalkFailure = (
kind: "escapes" | "not-file" | "loop",
path: string,
): ElectronFusePathFailure =>
new ElectronFusePathFailure(
"invalid_value",
kind === "escapes"
? `Framework symlink escapes ${FRAMEWORK_NAME}: ${path}`
: kind === "loop"
? `Framework symlink hop limit exceeded: ${path}`
: `Electron Framework path is not a regular file: ${path}`,
);
const frameworkRootOf = (path: string): string | null => {
let current = dirname(path);
while (basename(current) !== FRAMEWORK_NAME) {
const parent = dirname(current);
if (parent === current) return null;
current = parent;
}
return current;
};
const relativeInside = (root: string, path: string): string | null => {
// Keep dot segments until resolveInside has followed preceding symlinks.
// Lexical normalization would select a different file for link/../ paths.
const prefix = root.endsWith(sep) ? root : `${root}${sep}`;
if (!path.startsWith(prefix)) return null;
const value = path.slice(prefix.length);
return value === "" ? null : value.split(sep).join("/");
};
const isMissing = (cause: unknown): boolean =>
cause instanceof Error &&
"code" in cause &&
["ENOENT", "ENOTDIR"].includes(String(cause.code));

View File

@ -0,0 +1,684 @@
import { resolveElectronFuseTarget } from "./ElectronFusePath.js";
import { RegularFileChangedError } from "../filesystem/RegularFile.js";
import { mkdir, symlink, writeFile, truncate, rename } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import {
elfFuseImage,
fuseWire,
FUSE_SENTINEL,
OBSIDIAN_FUSE_BYTES,
peFuseImage,
sentinelFreeMacho,
thinFuseImage,
thinMacho,
truncatedFuseImage,
twoSentinelElf,
universalFuseImage,
} from "../../tests/fixtures/electron/fuseImage.js";
import { ELECTRON_FUSE_STATE_BYTES } from "../domain/electron/electronFuses.js";
import { ElectronFuseProvider } from "./ElectronFuseProvider.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { readElectronFuseImage } from "./ElectronFuseReader.js";
const provider = new ElectronFuseProvider();
const inspect = async (path: string) => {
const result = await provider.inspect({ path });
if (!result.ok) throw result.error;
return result.value;
};
describe("Electron fuse wire reading", () => {
it("reads thin Mach-O, universal Mach-O, PE, and ELF wires", async () => {
const root = await createTestTempDirectory("rea-electron-fuses-");
const thin = thinFuseImage(OBSIDIAN_FUSE_BYTES);
const universal = universalFuseImage();
const pe = peFuseImage(OBSIDIAN_FUSE_BYTES);
const elf = elfFuseImage([
ELECTRON_FUSE_STATE_BYTES.enabled,
ELECTRON_FUSE_STATE_BYTES.removed,
0x99,
]);
await writeFile(join(root, "thin"), thin.bytes);
await writeFile(join(root, "universal"), universal.bytes);
await writeFile(join(root, "app.exe"), pe.bytes);
await writeFile(join(root, "app.elf"), elf.bytes);
const thinReport = await inspect(join(root, "thin"));
expect(thinReport.artifact).toMatchObject({
format: "mach-o",
bytes: thin.bytes.length,
});
expect(thinReport.slices).toMatchObject([
{
architecture: "arm64",
sentinel_count: 1,
sentinels: [
{
sentinel_offset: thin.sentinelOffset,
wire_version: 1,
declared_length: 8,
complete: true,
},
],
},
]);
expect(
thinReport.slices[0]?.sentinels[0]?.fuses.map((fuse) => fuse.state),
).toEqual([
"disabled",
"enabled",
"disabled",
"disabled",
"disabled",
"disabled",
"disabled",
"enabled",
]);
const universalReport = await inspect(join(root, "universal"));
expect(universalReport.artifact.format).toBe("mach-o-universal");
expect(universalReport.slices).toMatchObject([
{
file_offset: universal.left.fileOffset,
architecture: "x86_64",
sentinels: [{ sentinel_offset: universal.left.sentinelOffset }],
},
{
file_offset: universal.right.fileOffset,
architecture: "arm64",
sentinels: [{ sentinel_offset: universal.right.sentinelOffset }],
},
]);
expect(
universalReport.runtime_implications.filter(
(item) => item.subject === "node_cli_inspect_arguments",
),
).toEqual([
expect.objectContaining({
slice_index: 0,
outcome: "not_honored",
sentinel_offset: universal.left.sentinelOffset,
}),
expect.objectContaining({
slice_index: 1,
outcome: "honored",
sentinel_offset: universal.right.sentinelOffset,
}),
]);
const peReport = await inspect(join(root, "app.exe"));
expect(peReport.artifact.format).toBe("pe");
expect(peReport.slices[0]?.sentinels[0]?.sentinel_offset).toBe(
pe.sentinelOffset,
);
const elfReport = await inspect(join(root, "app.elf"));
expect(elfReport.slices[0]?.sentinels[0]?.fuses).toMatchObject([
{ name: "RunAsNode", state: "enabled", raw_byte: 0x31 },
{ name: "EnableCookieEncryption", state: "removed", raw_byte: 0x72 },
{
name: "EnableNodeOptionsEnvironmentVariable",
state: "unknown",
raw_byte: 0x99,
},
]);
expect(elfReport.runtime_implications).toEqual([
expect.objectContaining({
subject: "electron_run_as_node",
outcome: "honored",
}),
]);
});
it("keeps a truncated wire, duplicate sentinels, and a missing sentinel", async () => {
const root = await createTestTempDirectory("rea-electron-fuses-partial-");
await writeFile(join(root, "truncated"), truncatedFuseImage());
const duplicated = twoSentinelElf();
await writeFile(join(root, "duplicated"), duplicated.bytes);
await writeFile(join(root, "empty"), sentinelFreeMacho());
const truncated = await inspect(join(root, "truncated"));
expect(truncated.slices[0]?.sentinels[0]).toMatchObject({
wire_version: 1,
declared_length: 4,
complete: false,
fuses: [{ index: 0, state: "enabled", raw_byte: 0x31 }],
});
expect(truncated.limitations.join(" ")).toContain("truncated");
const duplicates = await inspect(join(root, "duplicated"));
expect(duplicates.slices[0]?.sentinel_count).toBe(2);
expect(
duplicates.slices[0]?.sentinels.map((item) => item.sentinel_offset),
).toEqual([...duplicated.offsets]);
expect(duplicates.limitations.join(" ")).toContain("none is selected");
const missing = await inspect(join(root, "empty"));
expect(missing.slices[0]?.sentinel_count).toBe(0);
expect(missing.runtime_implications).toEqual([]);
expect(missing.limitations.join(" ")).toContain("unknown, not disabled");
});
});
describe("ELF embedded ASAR integrity applicability", () => {
it("preserves an enabled ASAR integrity fuse without claiming enforcement on ELF", async () => {
const root = await createTestTempDirectory("rea-electron-fuses-elf-asar-");
const fuses = [
ELECTRON_FUSE_STATE_BYTES.disabled,
ELECTRON_FUSE_STATE_BYTES.disabled,
ELECTRON_FUSE_STATE_BYTES.disabled,
ELECTRON_FUSE_STATE_BYTES.disabled,
ELECTRON_FUSE_STATE_BYTES.enabled,
];
const image = elfFuseImage(fuses);
const path = join(root, "electron");
await writeFile(path, image.bytes);
const report = await inspect(path);
expect(report.artifact.format).toBe("elf");
expect(report.slices[0]?.sentinels[0]?.fuses[4]).toMatchObject({
index: 4,
name: "EnableEmbeddedAsarIntegrityValidation",
state: "enabled",
raw_byte: ELECTRON_FUSE_STATE_BYTES.enabled,
});
expect(
report.runtime_implications.find(
(item) => item.subject === "embedded_asar_integrity_validation",
),
).toMatchObject({
relationship: "derivation",
fuse_index: 4,
outcome: "unknown",
sentinel_offset: image.sentinelOffset,
});
expect(report.limitations.join(" ")).toContain("only on macOS and Windows");
});
});
describe("Electron fuse reader resource boundaries", () => {
it("rejects invalid scan window sizes before allocating or scanning", async () => {
const root = await createTestTempDirectory(
"rea-electron-fuses-window-limit-",
);
const path = join(root, "window");
await writeFile(path, thinFuseImage(OBSIDIAN_FUSE_BYTES).bytes);
for (const chunkBytes of [0, -1, 1.5, 256 * 1024 + 1])
await expect(readElectronFuseImage(path, { chunkBytes })).rejects.toThrow(
/chunkBytes must be an integer between/,
);
});
it("rejects overlapping universal Mach-O slices before repeated searching", async () => {
const root = await createTestTempDirectory("rea-electron-fuses-overlap-");
const image = universalFuseImage().bytes;
const firstOffset = image.readUInt32BE(16);
const firstSize = image.readUInt32BE(20);
image.writeUInt32BE(0x01000007, 28);
image.writeUInt32BE(firstOffset, 36);
image.writeUInt32BE(firstSize, 40);
const path = join(root, "overlapping");
await writeFile(path, image);
const result = await provider.inspect({ path });
expect(result.ok).toBe(false);
if (!result.ok)
expect(result.error).toMatchObject({
_tag: "AnalysisInputError",
issues: [
expect.objectContaining({
path: ["path"],
reason: "invalid_format",
message: expect.stringContaining("slices 0 and 1 overlap"),
}),
],
});
});
});
describe("Electron fuse path and container admission", () => {
it("finds a sentinel that crosses a read window", async () => {
const root = await createTestTempDirectory("rea-electron-fuses-window-");
const image = thinFuseImage(OBSIDIAN_FUSE_BYTES);
const path = join(root, "window");
await writeFile(path, image.bytes);
const read = await readElectronFuseImage(path, { chunkBytes: 20 });
expect(read.slices[0]?.sentinels[0]?.sentinel_offset).toBe(
image.sentinelOffset,
);
expect(read.slices[0]?.sentinels[0]?.fuses).toHaveLength(8);
});
it("resolves an app bundle and a framework symlink inside the bundle", async () => {
const root = await createTestTempDirectory("rea-electron-fuses-app-");
const image = thinFuseImage(OBSIDIAN_FUSE_BYTES);
const app = join(root, "Fixture.app");
const versions = join(
app,
"Contents",
"Frameworks",
"Electron Framework.framework",
"Versions",
);
await mkdir(join(versions, "A"), { recursive: true });
const binary = join(versions, "A", "Electron Framework");
await writeFile(binary, image.bytes);
await symlink("A", join(versions, "Current"));
await symlink(
"Versions/Current/Electron Framework",
join(versions, "..", "Electron Framework"),
);
const fromApp = await inspect(app);
expect(fromApp.artifact.selected_path).toBe(app);
expect(fromApp.artifact.resolved_path).toBe(binary);
expect(fromApp.slices[0]?.sentinel_count).toBe(1);
const link = join(versions, "..", "Electron Framework");
const fromLink = await inspect(link);
expect(fromLink.artifact.resolved_path).toBe(binary);
expect(fromLink.artifact.sha256).toBe(fromApp.artifact.sha256);
});
it("rejects an app bundle whose Versions/Current symlink leaves the framework", async () => {
const root = await createTestTempDirectory(
"rea-electron-fuses-framework-root-",
);
const app = join(root, "Fixture.app");
const framework = join(
app,
"Contents",
"Frameworks",
"Electron Framework.framework",
);
await mkdir(join(framework, "Versions"), { recursive: true });
await mkdir(join(app, "Contents", "Other"), { recursive: true });
await writeFile(
join(app, "Contents", "Other", "Electron Framework"),
elfFuseImage([ELECTRON_FUSE_STATE_BYTES.enabled]).bytes,
);
await symlink("../../../Other", join(framework, "Versions", "Current"));
for (const path of [app, framework]) {
const result = await provider.inspect({ path });
expect(result.ok, path).toBe(false);
if (result.ok) continue;
expect(result.error).toMatchObject({
_tag: "AnalysisInputError",
issues: [
expect.objectContaining({
path: ["path"],
message: expect.stringContaining("escapes"),
}),
],
});
}
});
it("refuses a framework symlink that leaves the bundle", async () => {
const root = await createTestTempDirectory("rea-electron-fuses-escape-");
const versions = join(
root,
"Fixture.app",
"Contents",
"Frameworks",
"Electron Framework.framework",
"Versions",
);
await mkdir(versions, { recursive: true });
await symlink("/etc", join(versions, "Current"));
const result = await provider.inspect({
path: join(root, "Fixture.app"),
});
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error).toMatchObject({
_tag: "AnalysisInputError",
issues: [
expect.objectContaining({
path: ["path"],
message: expect.stringContaining("escapes"),
}),
],
});
});
it("rejects an unrecognized container and a big-endian Mach-O image", async () => {
const root = await createTestTempDirectory("rea-electron-fuses-reject-");
const text = join(root, "notes.txt");
const bigEndian = join(root, "big");
const java = join(root, "Main.class");
await writeFile(text, "This is not an executable container.");
const header = Buffer.alloc(32);
header.writeUInt32BE(0xfeedfacf, 0);
await writeFile(bigEndian, header);
await writeFile(
java,
Buffer.from([0xca, 0xfe, 0xba, 0xbe, 0x00, 0x00, 0x00, 0x37]),
);
for (const path of [text, bigEndian, java]) {
const result = await provider.inspect({ path });
expect(result.ok, path).toBe(false);
if (!result.ok)
expect(result.error._tag).toBe("AnalysisUnsupportedTargetError");
}
});
it("does not invent a sentinel from bytes that only straddle two slices", async () => {
const root = await createTestTempDirectory("rea-electron-fuses-span-");
const left = thinMacho(0x01000007, Buffer.alloc(16));
const right = thinMacho(0x0100000c, Buffer.alloc(16));
const overhang = 10;
FUSE_SENTINEL.copy(left, left.length - overhang);
const gap = Buffer.alloc(FUSE_SENTINEL.length - overhang);
FUSE_SENTINEL.copy(gap, 0, overhang);
const tableEnd = 48;
const rightOffset = tableEnd + left.length + gap.length;
const table = Buffer.alloc(tableEnd);
table.writeUInt32BE(0xcafebabe, 0);
table.writeUInt32BE(2, 4);
table.writeUInt32BE(0x01000007, 8);
table.writeUInt32BE(tableEnd, 16);
table.writeUInt32BE(left.length, 20);
table.writeUInt32BE(0x0100000c, 28);
table.writeUInt32BE(rightOffset, 36);
table.writeUInt32BE(right.length, 40);
const path = join(root, "split");
await writeFile(path, Buffer.concat([table, left, gap, right]));
const report = await inspect(path);
expect(report.slices.map((item) => item.sentinel_count)).toEqual([0, 0]);
});
});
describe("direct Electron Framework binary selection", () => {
const frameworkFixture = async (prefix: string) => {
const root = await createTestTempDirectory(prefix);
const app = join(root, "Fixture.app");
const framework = join(
app,
"Contents",
"Frameworks",
"Electron Framework.framework",
);
await mkdir(join(framework, "Versions"), { recursive: true });
return { app, framework };
};
it("resolves a binary selected through Versions/Current to its target", async () => {
const { framework } = await frameworkFixture("rea-electron-fuses-direct-");
await mkdir(join(framework, "Versions", "A"), { recursive: true });
const binary = join(framework, "Versions", "A", "Electron Framework");
await writeFile(binary, thinFuseImage(OBSIDIAN_FUSE_BYTES).bytes);
await symlink("A", join(framework, "Versions", "Current"));
const selected = join(
framework,
"Versions",
"Current",
"Electron Framework",
);
const report = await inspect(selected);
expect(report.artifact.selected_path).toBe(selected);
expect(report.artifact.resolved_path).toBe(binary);
});
it("follows symlinks before parent segments in a selected binary path", async () => {
const { framework } = await frameworkFixture("rea-electron-fuses-parent-");
const versions = join(framework, "Versions");
await mkdir(join(versions, "nested", "B"), { recursive: true });
await mkdir(join(versions, "nested", "A"), { recursive: true });
await mkdir(join(versions, "A"), { recursive: true });
const binary = join(versions, "nested", "A", "Electron Framework");
await writeFile(binary, thinFuseImage(OBSIDIAN_FUSE_BYTES).bytes);
await writeFile(
join(versions, "A", "Electron Framework"),
sentinelFreeMacho(),
);
await symlink("nested/B", join(versions, "Current"));
const selected = `${versions}/Current/../A/Electron Framework`;
const report = await inspect(selected);
expect(report.artifact.selected_path).toBe(selected);
expect(report.artifact.resolved_path).toBe(binary);
expect(report.slices[0]?.sentinel_count).toBe(1);
});
it("rejects a binary selected through a Versions/Current link that leaves the framework", async () => {
const { app, framework } = await frameworkFixture(
"rea-electron-fuses-direct-escape-",
);
await mkdir(join(app, "Contents", "Other"), { recursive: true });
await writeFile(
join(app, "Contents", "Other", "Electron Framework"),
elfFuseImage([ELECTRON_FUSE_STATE_BYTES.enabled]).bytes,
);
await symlink("../../../Other", join(framework, "Versions", "Current"));
const result = await provider.inspect({
path: join(framework, "Versions", "Current", "Electron Framework"),
});
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error).toMatchObject({
_tag: "AnalysisInputError",
issues: [
expect.objectContaining({
path: ["path"],
message: expect.stringContaining("escapes"),
}),
],
});
});
it("rejects a framework directory that is a symlink outside the bundle", async () => {
const root = await createTestTempDirectory(
"rea-electron-fuses-framework-link-",
);
const outside = join(root, "outside");
await mkdir(join(outside, "Versions", "A"), { recursive: true });
await writeFile(
join(outside, "Versions", "A", "Electron Framework"),
thinFuseImage(OBSIDIAN_FUSE_BYTES).bytes,
);
const app = join(root, "Example.app");
const frameworks = join(app, "Contents", "Frameworks");
await mkdir(frameworks, { recursive: true });
const framework = join(frameworks, "Electron Framework.framework");
await symlink(outside, framework);
for (const path of [
app,
join(framework, "Versions", "A", "Electron Framework"),
]) {
const result = await provider.inspect({ path });
expect(result.ok, path).toBe(false);
if (result.ok) continue;
expect(result.error).toMatchObject({
_tag: "AnalysisInputError",
issues: [
expect.objectContaining({
path: ["path"],
message: expect.stringContaining("escapes"),
}),
],
});
}
});
});
it.each(["Contents", "Frameworks"])(
"rejects a redirected %s for every app selection form",
async (segment) => {
const root = await createTestTempDirectory("rea-electron-parent-link-");
const app = join(root, "Fixture.app");
const outside = join(root, "outside");
const actualFramework = join(
outside,
...(segment === "Contents" ? ["Frameworks"] : []),
"Electron Framework.framework",
);
await mkdir(join(actualFramework, "Versions", "A"), { recursive: true });
await writeFile(
join(actualFramework, "Versions", "A", "Electron Framework"),
thinFuseImage(OBSIDIAN_FUSE_BYTES).bytes,
);
await symlink("A", join(actualFramework, "Versions", "Current"));
await mkdir(segment === "Contents" ? app : join(app, "Contents"), {
recursive: true,
});
await symlink(
outside,
join(
app,
"Contents",
...(segment === "Frameworks" ? ["Frameworks"] : []),
),
);
const framework = join(
app,
"Contents",
"Frameworks",
"Electron Framework.framework",
);
for (const path of [
app,
framework,
join(framework, "Versions", "Current", "Electron Framework"),
]) {
const result = await provider.inspect({ path });
expect(result.ok, path).toBe(false);
if (!result.ok) expect(result.error._tag).toBe("AnalysisInputError");
}
},
);
it("enforces the 32 sentinel boundary with typed and public limit details", async () => {
const root = await createTestTempDirectory("rea-electron-sentinel-limit-");
const wire = Buffer.concat([FUSE_SENTINEL, fuseWire(1, [0x31])]);
for (const count of [32, 33]) {
const path = join(root, String(count));
await writeFile(
path,
thinMacho(
0x0100000c,
Buffer.concat(Array.from({ length: count }, () => wire)),
),
);
const result = await provider.inspect({ path });
if (count === 32) {
expect(result.ok).toBe(true);
if (result.ok) expect(result.value.slices[0]?.sentinel_count).toBe(32);
} else {
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error._tag).toBe("AnalysisResourceConstraintError");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "resource_constraint",
details: { reported_limits: { maximum_sentinels_per_slice: 32 } },
});
}
}
}
});
it("rejects a sparse artifact over 1 GiB before scanning", async () => {
const root = await createTestTempDirectory("rea-electron-input-limit-");
const path = join(root, "large");
await writeFile(path, thinFuseImage(OBSIDIAN_FUSE_BYTES).bytes);
await truncate(path, 1024 * 1024 * 1024 + 1);
const result = await provider.inspect({ path });
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error._tag).toBe("AnalysisResourceConstraintError");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "resource_constraint",
details: { reported_limits: { maximum_input_bytes: 1024 * 1024 * 1024 } },
});
}
});
it("follows the active framework binary while an inactive A version remains", async () => {
const root = await createTestTempDirectory("rea-electron-current-version-");
const app = join(root, "Fixture.app");
const framework = join(
app,
"Contents",
"Frameworks",
"Electron Framework.framework",
);
for (const [version, state] of [
["A", 0x30],
["B", 0x31],
] as const) {
await mkdir(join(framework, "Versions", version), { recursive: true });
await writeFile(
join(framework, "Versions", version, "Electron Framework"),
thinFuseImage([state]).bytes,
);
}
await symlink("B", join(framework, "Versions", "Current"));
await symlink(
"Versions/Current/Electron Framework",
join(framework, "Electron Framework"),
);
for (const path of [app, framework, join(framework, "Electron Framework")]) {
const report = await inspect(path);
expect(report.artifact.resolved_path).toBe(
join(framework, "Versions", "B", "Electron Framework"),
);
expect(report.slices[0]?.sentinels[0]?.fuses[0]?.state).toBe("enabled");
}
});
it("rejects a checked version directory swapped before the reader opens it", async () => {
const root = await createTestTempDirectory("rea-electron-directory-swap-");
const app = join(root, "Fixture.app");
const framework = join(
app,
"Contents",
"Frameworks",
"Electron Framework.framework",
);
const version = join(framework, "Versions", "A");
const outside = join(root, "outside");
await mkdir(version, { recursive: true });
await mkdir(outside);
await writeFile(
join(version, "Electron Framework"),
thinFuseImage([0x30]).bytes,
);
await writeFile(
join(outside, "Electron Framework"),
thinFuseImage([0x31]).bytes,
);
const located = await resolveElectronFuseTarget(app);
await rename(version, `${version}.saved`);
await symlink(outside, version);
await expect(
readElectronFuseImage(located.file.path, { selection: located.file }),
).rejects.toBeInstanceOf(RegularFileChangedError);
});
it("reports a checked version directory removed before open as changed", async () => {
const root = await createTestTempDirectory("rea-electron-directory-missing-");
const app = join(root, "Fixture.app");
const version = join(
app,
"Contents",
"Frameworks",
"Electron Framework.framework",
"Versions",
"A",
);
await mkdir(version, { recursive: true });
await writeFile(
join(version, "Electron Framework"),
thinFuseImage([0x30]).bytes,
);
const located = await resolveElectronFuseTarget(app);
await rename(version, `${version}.saved`);
await expect(
readElectronFuseImage(located.file.path, { selection: located.file }),
).rejects.toBeInstanceOf(RegularFileChangedError);
});

View File

@ -0,0 +1,170 @@
import type { ExecutionOptions } from "../application/AnalysisProvider.js";
import type { ElectronFusePort } from "../application/electron/ElectronFusePort.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import {
AnalysisAccessDeniedError,
AnalysisArtifactChangedError,
AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError,
AnalysisResourceConstraintError,
AnalysisUnsupportedTargetError,
} from "../domain/analysisErrorCore.js";
import {
describeElectronFuseFindings,
electronFuseReportSchema,
type ElectronFuseReport,
type InspectElectronFusesInput,
} from "../domain/electron/electronFuses.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { err, ok, type Result } from "../domain/result.js";
import { ZodError } from "zod";
import { RegularFileChangedError } from "../filesystem/RegularFile.js";
import { NonRegularFileReadError } from "../filesystem/RegularFile.js";
import {
ElectronFusePathFailure,
resolveElectronFuseTarget,
} from "./ElectronFusePath.js";
import {
electronFuseReadLimitation,
ElectronFuseFormatFailure,
ElectronFuseResourceFailure,
readElectronFuseImage,
} from "./ElectronFuseReader.js";
/** Bundled fuse-wire reader. It does not flip fuses or launch the application. */
export const ELECTRON_FUSE_PROVIDER_IDENTITY = {
id: "electron-fuses",
name: "REA Electron fuse reader",
version: "1",
} as const;
const OPERATION = "inspect_electron_fuses";
/** Read packaged Electron fuse wires from one selected local file or app bundle. */
export class ElectronFuseProvider implements ElectronFusePort {
readonly identity = ELECTRON_FUSE_PROVIDER_IDENTITY;
async inspect(
input: InspectElectronFusesInput,
options?: ExecutionOptions,
): Promise<Result<ElectronFuseReport, AnalysisError>> {
try {
options?.signal?.throwIfAborted();
const located = await resolveElectronFuseTarget(
input.path,
options?.signal,
);
options?.signal?.throwIfAborted();
const image = await readElectronFuseImage(located.file.path, {
selection: located.file,
...(options?.signal === undefined ? {} : { signal: options.signal }),
});
const findings = describeElectronFuseFindings(image.slices, image.format);
const parsed = electronFuseReportSchema.safeParse({
artifact: {
selected_path: located.selectedPath,
resolved_path: located.file.path,
sha256: image.sha256,
bytes: image.bytes,
format: image.format,
},
slices: image.slices,
runtime_implications: findings.runtime_implications,
limitations: [...findings.limitations, electronFuseReadLimitation()],
});
if (!parsed.success)
throw new AnalysisOutputError(
OPERATION,
"Electron fuse reader produced a report outside its schema.",
{ cause: parsed.error },
);
options?.signal?.throwIfAborted();
return ok(parsed.data);
} catch (cause: unknown) {
return err(failureFor(cause, input.path, options?.signal));
}
}
}
const failureFor = (
cause: unknown,
path: string,
signal?: AbortSignal,
): AnalysisError => {
if (cause instanceof AnalysisError) return cause;
if (signal?.aborted) return new AnalysisCancelledError(OPERATION);
if (cause instanceof ElectronFusePathFailure)
return new AnalysisInputError(OPERATION, { cause }, [
{ path: ["path"], reason: cause.reason, message: cause.message },
]);
if (cause instanceof ElectronFuseResourceFailure)
return new AnalysisResourceConstraintError(
OPERATION,
cause.boundary === "file-size" ? "file-size" : "capacity",
cause.message,
cause.boundary === "file-size"
? { maximum_input_bytes: 1024 * 1024 * 1024 }
: { maximum_sentinels_per_slice: 32 },
{ cause },
);
if (cause instanceof ElectronFuseFormatFailure)
return cause.kind === "unsupported"
? new AnalysisUnsupportedTargetError(OPERATION, path, cause.message, {
cause,
})
: new AnalysisInputError(OPERATION, { cause }, [
{
path: ["path"],
reason: "invalid_format",
message: `Selected artifact ${path}: ${cause.message}`,
},
]);
if (cause instanceof RegularFileChangedError)
return new AnalysisArtifactChangedError(OPERATION, path, cause.message, {
cause,
});
if (cause instanceof NonRegularFileReadError)
return new AnalysisInputError(OPERATION, { cause }, [
{
path: ["path"],
reason: "invalid_format",
message: cause.message,
},
]);
if (cause instanceof ZodError)
return new AnalysisOutputError(
OPERATION,
"Electron fuse reader produced a report outside its schema.",
{ cause },
);
if (cause instanceof Error && "code" in cause) {
if (cause.code === "EACCES" || cause.code === "EPERM")
return new AnalysisAccessDeniedError(OPERATION, path, cause.code, {
cause,
});
if (
["ENOENT", "ENOTDIR", "ELOOP", "ENAMETOOLONG"].includes(
String(cause.code),
)
)
return new AnalysisInputError(OPERATION, { cause }, [
{
path: ["path"],
reason: "invalid_value",
message: `Selected artifact could not be read (${String(cause.code)}): ${path}.`,
},
]);
}
return new ProviderAdapterError(
ELECTRON_FUSE_PROVIDER_IDENTITY.id,
OPERATION,
{
cause,
diagnostics: {
path,
reason: cause instanceof Error ? cause.message : String(cause),
},
},
);
};

View File

@ -0,0 +1,480 @@
import type { ResolvedElectronFuseFile } from "./ElectronFusePath.js";
import { createHash } from "node:crypto";
import { lstat } from "node:fs/promises";
import {
hasMachoMagic,
readMachoImage,
} from "../artifacts/apple/MachoLoadCommandReader.js";
import type { ReadAt } from "../artifacts/apple/MachoLoadCommandReader.js";
import {
electronFuseName,
electronFuseState,
ELECTRON_FUSE_SENTINEL,
type ElectronFuseSentinel,
type ElectronFuseSlice,
} from "../domain/electron/electronFuses.js";
import {
openRegularFile,
RegularFileChangedError,
sameRegularFileState,
} from "../filesystem/RegularFile.js";
/** Admission and search bounds for one Electron executable. */
export const ELECTRON_FUSE_LIMITS = {
maximumFileBytes: 1024 * 1024 * 1024,
chunkBytes: 256 * 1024,
maximumSentinelsPerSlice: 32,
maximumPeHeaderOffset: 1024 * 1024,
} as const;
const SENTINEL = Buffer.from(ELECTRON_FUSE_SENTINEL, "ascii");
const FAT_MAGICS = new Set([0xcafebabe, 0xcafebabf, 0xbebafeca, 0xbfbafeca]);
/** Container identity and per-slice fuse wires. Offsets are file-absolute. */
export interface ElectronFuseImage {
readonly sha256: string;
readonly bytes: number;
readonly format: "mach-o" | "mach-o-universal" | "pe" | "elf";
readonly slices: readonly ElectronFuseSlice[];
}
type ElectronFuseContainerFormat = ElectronFuseImage["format"];
interface SliceRange {
readonly index: number;
readonly fileOffset: number;
readonly size: number;
readonly architecture: string | null;
}
/** The selected container is malformed or outside the supported formats. */
export class ElectronFuseFormatFailure extends Error {
constructor(
readonly kind: "malformed" | "unsupported",
message: string,
) {
super(message);
this.name = "ElectronFuseFormatFailure";
}
}
/** A measured bound was hit before a complete fuse report could be returned. */
export class ElectronFuseResourceFailure extends Error {
constructor(
readonly boundary: "file-size" | "capacity",
message: string,
) {
super(message);
this.name = "ElectronFuseResourceFailure";
}
}
/**
* Hash one admitted regular file and search each slice for the fuse sentinel.
* The search keeps only a sentinel-sized overlap between chunks.
*/
export const readElectronFuseImage = async (
path: string,
options?: {
readonly signal?: AbortSignal;
readonly chunkBytes?: number;
readonly selection?: ResolvedElectronFuseFile;
},
): Promise<ElectronFuseImage> => {
const signal = options?.signal;
signal?.throwIfAborted();
const selection = options?.selection;
const recheckPath = async (candidate: string) => {
signal?.throwIfAborted();
try {
const metadata = await lstat(candidate, { bigint: true });
signal?.throwIfAborted();
return metadata;
} catch (cause: unknown) {
if (selection !== undefined && isMissingPath(cause))
throw new RegularFileChangedError(path);
throw cause;
}
};
const assertPathNodes = async (): Promise<void> => {
if (selection === undefined) return;
if (selection.path !== path) throw new RegularFileChangedError(path);
for (const node of selection.nodes) {
const current = await recheckPath(node.path);
if (
current.dev !== node.metadata.dev ||
current.ino !== node.metadata.ino ||
current.isDirectory() !== node.metadata.isDirectory() ||
current.isSymbolicLink() !== node.metadata.isSymbolicLink()
)
throw new RegularFileChangedError(path);
}
};
await assertPathNodes();
const before = await recheckPath(path);
if (
selection !== undefined &&
!sameRegularFileState(selection.metadata, before)
)
throw new RegularFileChangedError(path);
if (!before.isFile() || before.isSymbolicLink())
throw new ElectronFuseFormatFailure(
"unsupported",
`Expected a regular file without a symlink: ${path}`,
);
if (before.size > BigInt(ELECTRON_FUSE_LIMITS.maximumFileBytes))
throw new ElectronFuseResourceFailure(
"file-size",
`Selected artifact exceeds ${ELECTRON_FUSE_LIMITS.maximumFileBytes} bytes: ${path}`,
);
const size = Number(before.size);
const chunkBytes = options?.chunkBytes ?? ELECTRON_FUSE_LIMITS.chunkBytes;
if (
!Number.isSafeInteger(chunkBytes) ||
chunkBytes < 1 ||
chunkBytes > ELECTRON_FUSE_LIMITS.chunkBytes
)
throw new TypeError(
`chunkBytes must be an integer between 1 and ${ELECTRON_FUSE_LIMITS.chunkBytes}.`,
);
const handle = await openRegularFile(path, { symlinks: "reject", signal });
try {
const opened = await handle.stat({ bigint: true });
if (!sameRegularFileState(before, opened))
throw new RegularFileChangedError(path);
await assertPathNodes();
const readAt = readerFor(handle, size, path, signal);
const container = await identifyContainer(readAt, size, path);
const scanned = await scanSlices(
handle,
path,
size,
container.slices,
chunkBytes,
signal,
);
const current = await recheckPath(path);
if (!sameRegularFileState(before, current))
throw new RegularFileChangedError(path);
await assertPathNodes();
return {
sha256: scanned.sha256,
bytes: size,
format: container.format,
slices: scanned.slices,
};
} finally {
await handle.close();
}
};
/** Sentence describing the file and sentinel bounds enforced by this reader. */
export const electronFuseReadLimitation = (): string =>
`The selected file is admitted as one regular file of at most 1 GiB. Sentinel search uses ${ELECTRON_FUSE_LIMITS.chunkBytes}-byte windows, and each slice keeps at most ${ELECTRON_FUSE_LIMITS.maximumSentinelsPerSlice} sentinels.`;
const readerFor =
(
handle: Awaited<ReturnType<typeof openRegularFile>>,
size: number,
path: string,
signal: AbortSignal | undefined,
): ReadAt =>
async (offset, length) => {
signal?.throwIfAborted();
if (length <= 0 || offset < 0 || offset >= size) return new Uint8Array();
const bounded = Math.min(length, size - offset);
const buffer = Buffer.alloc(bounded);
const read = await handle.read(buffer, 0, bounded, offset);
if (read.bytesRead !== bounded) throw new RegularFileChangedError(path);
return buffer.subarray(0, read.bytesRead);
};
const identifyContainer = async (
readAt: ReadAt,
size: number,
path: string,
): Promise<{
readonly format: ElectronFuseContainerFormat;
readonly slices: readonly SliceRange[];
}> => {
const head = await readAt(0, Math.min(size, 8));
if (hasMachoMagic(head)) return identifyMacho(readAt, size, head, path);
if (isElf(head))
return {
format: "elf",
slices: [{ index: 0, fileOffset: 0, size, architecture: null }],
};
if (await isPe(readAt, size))
return {
format: "pe",
slices: [{ index: 0, fileOffset: 0, size, architecture: null }],
};
throw new ElectronFuseFormatFailure(
"unsupported",
`Selected artifact ${path} does not contain a Mach-O, ELF, or PE container.`,
);
};
const identifyMacho = async (
readAt: ReadAt,
size: number,
head: Uint8Array,
path: string,
): Promise<{
readonly format: ElectronFuseContainerFormat;
readonly slices: readonly SliceRange[];
}> => {
const facts = await readMachoImage(readAt, size);
if (facts.status !== "parsed") {
const reason =
facts.status === "not-mach-o"
? `Selected artifact ${path} does not contain a Mach-O, ELF, or PE container.`
: facts.reason;
throw new ElectronFuseFormatFailure(
facts.status === "malformed" ? "malformed" : "unsupported",
reason,
);
}
const format = isFat(head) ? "mach-o-universal" : "mach-o";
const slices = facts.slices.map((slice, index) => ({
index,
fileOffset: slice.slice_offset,
size: slice.slice_size,
architecture: slice.architecture,
}));
assertNonOverlappingSlices(slices);
return {
format,
slices,
};
};
/** A FAT architecture owns a distinct byte range; overlap is malformed. */
const assertNonOverlappingSlices = (ranges: readonly SliceRange[]): void => {
const ordered = [...ranges].sort(
(left, right) => left.fileOffset - right.fileOffset,
);
for (let index = 1; index < ordered.length; index += 1) {
const previous = ordered[index - 1];
const current = ordered[index];
if (
previous !== undefined &&
current !== undefined &&
current.fileOffset < previous.fileOffset + previous.size
)
throw new ElectronFuseFormatFailure(
"malformed",
`Mach-O slices ${previous.index} and ${current.index} overlap.`,
);
}
};
const scanSlices = async (
handle: Awaited<ReturnType<typeof openRegularFile>>,
path: string,
size: number,
ranges: readonly SliceRange[],
chunkBytes: number,
signal: AbortSignal | undefined,
): Promise<{
readonly sha256: string;
readonly slices: readonly ElectronFuseSlice[];
}> => {
const states = ranges.map((slice) => ({
slice,
carry: Buffer.alloc(0),
hits: [] as number[],
}));
const hash = createHash("sha256");
const chunk = Buffer.allocUnsafe(chunkBytes);
let position = 0;
while (position < size) {
signal?.throwIfAborted();
const length = Math.min(chunkBytes, size - position);
const read = await handle.read(chunk, 0, length, position);
if (read.bytesRead !== length) throw new RegularFileChangedError(path);
const piece = chunk.subarray(0, length);
hash.update(piece);
absorbChunk(states, piece, position);
position += length;
}
if (position !== size) throw new RegularFileChangedError(path);
const slices: ElectronFuseSlice[] = [];
for (const state of states) {
const sentinels: ElectronFuseSentinel[] = [];
for (const offset of state.hits)
sentinels.push(
await parseWire(
handle,
path,
offset,
state.slice.fileOffset + state.slice.size,
signal,
),
);
slices.push({
index: state.slice.index,
file_offset: state.slice.fileOffset,
size: state.slice.size,
architecture: state.slice.architecture,
sentinel_count: sentinels.length,
sentinels,
});
}
return { sha256: hash.digest("hex"), slices };
};
const absorbChunk = (
states: readonly {
readonly slice: SliceRange;
carry: Buffer;
readonly hits: number[];
}[],
chunk: Buffer,
position: number,
): void => {
const chunkEnd = position + chunk.length;
for (const state of states) {
const start = Math.max(position, state.slice.fileOffset);
const end = Math.min(chunkEnd, state.slice.fileOffset + state.slice.size);
if (start >= end) continue;
const piece = chunk.subarray(start - position, end - position);
const carryLength = state.carry.length;
const window =
carryLength === 0 ? piece : Buffer.concat([state.carry, piece]);
const windowFileStart = start - carryLength;
let from = 0;
while (from <= window.length - SENTINEL.length) {
const found = window.indexOf(SENTINEL, from);
if (found < 0) break;
const sentinelOffset = windowFileStart + found;
if (!state.hits.includes(sentinelOffset)) {
if (state.hits.length >= ELECTRON_FUSE_LIMITS.maximumSentinelsPerSlice)
throw new ElectronFuseResourceFailure(
"capacity",
`Slice ${state.slice.index} contains more than ${ELECTRON_FUSE_LIMITS.maximumSentinelsPerSlice} Electron fuse sentinels.`,
);
state.hits.push(sentinelOffset);
}
from = found + 1;
}
const tail = SENTINEL.length - 1;
state.carry =
window.length <= tail
? Buffer.from(window)
: Buffer.from(window.subarray(window.length - tail));
}
};
const parseWire = async (
handle: Awaited<ReturnType<typeof openRegularFile>>,
path: string,
sentinelOffset: number,
sliceEnd: number,
signal: AbortSignal | undefined,
): Promise<ElectronFuseSentinel> => {
const wireStart = sentinelOffset + SENTINEL.length;
const available = sliceEnd - wireStart;
if (available < 1)
return {
sentinel_offset: sentinelOffset,
wire_version: null,
declared_length: null,
fuses: [],
complete: false,
};
const header = await readExact(
handle,
path,
wireStart,
Math.min(available, 2),
signal,
);
const wireVersion = header[0];
if (wireVersion === undefined || header.length < 2)
return {
sentinel_offset: sentinelOffset,
wire_version: wireVersion ?? null,
declared_length: null,
fuses: [],
complete: false,
};
const declared = header[1] ?? 0;
const fuseLength = Math.min(declared, available - 2);
const fuseBytes =
fuseLength === 0
? Buffer.alloc(0)
: await readExact(handle, path, wireStart + 2, fuseLength, signal);
return {
sentinel_offset: sentinelOffset,
wire_version: wireVersion,
declared_length: declared,
fuses: [...fuseBytes].map((raw, index) => ({
index,
name: electronFuseName(wireVersion, index),
state: electronFuseState(raw),
raw_byte: raw,
})),
complete: fuseBytes.length === declared,
};
};
const readExact = async (
handle: Awaited<ReturnType<typeof openRegularFile>>,
path: string,
offset: number,
length: number,
signal: AbortSignal | undefined,
): Promise<Buffer> => {
signal?.throwIfAborted();
const buffer = Buffer.alloc(length);
const read = await handle.read(buffer, 0, length, offset);
if (read.bytesRead !== length) throw new RegularFileChangedError(path);
return buffer;
};
const isElf = (head: Uint8Array): boolean =>
head.length >= 4 &&
head[0] === 0x7f &&
head[1] === 0x45 &&
head[2] === 0x4c &&
head[3] === 0x46;
const isFat = (head: Uint8Array): boolean => {
if (head.length < 4) return false;
const magic = Buffer.from(head.buffer, head.byteOffset, 4).readUInt32BE(0);
return FAT_MAGICS.has(magic);
};
const isPe = async (readAt: ReadAt, size: number): Promise<boolean> => {
if (size < 64) return false;
const header = await readAt(0, 64);
if (header.length < 64 || header[0] !== 0x4d || header[1] !== 0x5a)
return false;
const offset = Buffer.from(
header.buffer,
header.byteOffset,
header.byteLength,
).readUInt32LE(0x3c);
if (
!Number.isSafeInteger(offset) ||
offset < 64 ||
offset > ELECTRON_FUSE_LIMITS.maximumPeHeaderOffset ||
offset > size - 4
)
return false;
const signature = await readAt(offset, 4);
return (
signature.length === 4 &&
signature[0] === 0x50 &&
signature[1] === 0x45 &&
signature[2] === 0 &&
signature[3] === 0
);
};
const isMissingPath = (cause: unknown): boolean =>
cause instanceof Error &&
"code" in cause &&
["ENOENT", "ENOTDIR"].includes(String(cause.code));

View File

@ -22,6 +22,8 @@ import { registerRecordedCrashTools } from "./registerRecordedCrashTools.js";
import { createRecordedCrashService } from "../composition/binaryDiagnostics.js";
import { createGoBinaryService } from "../composition/go.js";
import type { GoBinaryService } from "../application/go/GoBinaryService.js";
import { createElectronFuseService } from "../composition/electronFuses.js";
import type { ElectronFuseService } from "../application/electron/ElectronFuseService.js";
import { registerGoTools } from "./registerGoTools.js";
import { createRustBinaryService } from "../composition/rust.js";
import type { RustBinaryService } from "../application/rust/RustBinaryService.js";
@ -157,6 +159,7 @@ export interface CreateServerOptions {
readonly binaryLayout?: BinaryLayoutService;
readonly goBinary?: GoBinaryService;
readonly rustBinary?: RustBinaryService;
readonly electronFuses?: ElectronFuseService;
readonly recordedCrash?: RecordedCrashService;
readonly firmwareAnalysis?: FirmwareAnalysisPort;
readonly javascriptRecovery?: JavaScriptRecoveryPort;
@ -884,6 +887,7 @@ const registerObservationTools = ({
evidenceById,
electron: options.electronObservation,
electronActive: options.electronActiveObservation,
electronFuses: options.electronFuses ?? createElectronFuseService(),
observationLoadFailure:
options.optionalProviderLoadFailures?.electronObservation,
activeLoadFailure:

View File

@ -21,6 +21,7 @@ import { analyzeJavaScriptApplicationValidated } from "../application/javascript
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { reconcileJavaScriptRuntimeEvidenceValidated } from "../application/javascript/JavaScriptRuntimeReconciliationService.js";
import type { ProgressReporter } from "../application/ProgressReporter.js";
import type { ElectronFuseService } from "../application/electron/ElectronFuseService.js";
import { toolContract } from "../contracts/toolContracts.js";
import type { ToolContract } from "../contracts/toolContractTypes.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
@ -32,6 +33,7 @@ import { logToolExecution } from "./toolLogging.js";
import { toolRegistrationOptions } from "./toolRegistrationOptions.js";
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
import { runAdmittedToolOperation } from "./admittedToolOperation.js";
import { createEvidenceToolHandler } from "./contractToolHandler.js";
interface ElectronToolRegistration {
readonly artifactResources: ArtifactResourceScope;
@ -40,6 +42,7 @@ interface ElectronToolRegistration {
readonly activeLoadFailure?: OptionalProviderLoadFailure | undefined;
readonly electron: ElectronObservationPort | undefined;
readonly electronActive: ElectronActiveObservationPort | undefined;
readonly electronFuses: ElectronFuseService;
readonly recordEvidence: EvidenceWriter["recordEvidence"] | undefined;
readonly evidenceById: EvidenceLookup | undefined;
readonly withAdmittedAnalysis?: WithAdmittedAnalysis;
@ -57,6 +60,7 @@ export const registerElectronTools = (
options: ElectronToolRegistration,
): void => {
const registration = { ...options, delivery: server.delivery, server };
registerElectronFuseTool(server, options);
const listContract = toolContract("list_electron_targets");
const inspectContract = toolContract("inspect_electron_page");
const analyzeContract = toolContract("analyze_javascript_application");
@ -141,6 +145,25 @@ export const registerElectronTools = (
);
};
const registerElectronFuseTool = (
server: EvidenceMcpServer,
options: ElectronToolRegistration,
): void => {
const contract = toolContract("inspect_electron_fuses");
const handler = createEvidenceToolHandler(
server,
(input, execution) => options.electronFuses.inspect(input, execution),
options.logger,
options.recordEvidence,
options.withAdmittedAnalysis,
);
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
handler(contract),
);
};
const runElectronTool = async <Input>(
options: ElectronToolRegistration & {
readonly delivery: ToolResultDelivery;

View File

@ -0,0 +1,150 @@
import { mkdir, symlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { createHash } from "node:crypto";
import { expect } from "vitest";
import { toolContract } from "../../src/contracts/toolContracts.js";
import {
OBSIDIAN_FUSE_BYTES,
sentinelFreeMacho,
thinFuseImage,
universalFuseImage,
} from "../fixtures/electron/fuseImage.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { cliTest } from "../support/cli/cliFixture.js";
cliTest(
"reads thin and universal Electron fuse wires through the public CLI",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-electron-fuses-cli-");
const thin = thinFuseImage(OBSIDIAN_FUSE_BYTES);
const universal = universalFuseImage();
await writeFile(join(root, "thin"), thin.bytes);
await writeFile(join(root, "universal"), universal.bytes);
const thinResult = await cli.run({
arguments: ["inspect-electron-fuses", "thin", "--json"],
cwd: root,
});
expect(thinResult.exitCode, thinResult.stderr).toBe(0);
const thinEvidence = toolContract(
"inspect_electron_fuses",
).outputSchema.parse(thinResult.json);
expect(thinEvidence.subject?.local_path).toBe(join(root, "thin"));
expect(thinEvidence.subject?.digest.sha256).toBe(
createHash("sha256").update(thin.bytes).digest("hex"),
);
expect(thinEvidence.subject?.architecture).toBe("arm64");
expect(
thinEvidence.normalized_result.slices[0]?.sentinels[0],
).toMatchObject({
sentinel_offset: thin.sentinelOffset,
wire_version: 1,
declared_length: 8,
});
expect(
thinEvidence.normalized_result.runtime_implications.find(
(item) => item.subject === "node_cli_inspect_arguments",
),
).toMatchObject({ outcome: "not_honored", fuse_index: 3 });
const universalResult = await cli.run({
arguments: ["inspect-electron-fuses", join(root, "universal"), "--json"],
});
expect(universalResult.exitCode, universalResult.stderr).toBe(0);
const universalEvidence = toolContract(
"inspect_electron_fuses",
).outputSchema.parse(universalResult.json);
expect(universalEvidence.normalized_result.artifact.format).toBe(
"mach-o-universal",
);
expect(universalEvidence.subject?.architecture).toBeNull();
expect(
universalEvidence.normalized_result.slices.map((slice) => ({
file_offset: slice.file_offset,
sentinel_offset: slice.sentinels[0]?.sentinel_offset,
})),
).toEqual([
{
file_offset: universal.left.fileOffset,
sentinel_offset: universal.left.sentinelOffset,
},
{
file_offset: universal.right.fileOffset,
sentinel_offset: universal.right.sentinelOffset,
},
]);
},
);
cliTest(
"reports a missing sentinel as unknown fuse state through the public CLI",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-electron-fuses-cli-empty-");
const path = join(root, "empty");
await writeFile(path, sentinelFreeMacho());
const result = await cli.run({
arguments: ["inspect-electron-fuses", path, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract("inspect_electron_fuses").outputSchema.parse(
result.json,
);
expect(evidence.normalized_result.slices[0]?.sentinel_count).toBe(0);
expect(evidence.normalized_result.runtime_implications).toEqual([]);
expect(evidence.limitations.join(" ")).toContain(
"unknown, not that every fuse is disabled",
);
},
);
cliTest(
"resolves an Electron app bundle to its framework binary through the public CLI",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-electron-fuses-cli-app-");
const image = thinFuseImage(OBSIDIAN_FUSE_BYTES);
const app = join(root, "Fixture.app");
const binary = join(
app,
"Contents",
"Frameworks",
"Electron Framework.framework",
"Versions",
"A",
"Electron Framework",
);
await mkdir(join(binary, ".."), { recursive: true });
await writeFile(binary, image.bytes);
await symlink("A", join(binary, "..", "..", "Current"));
const result = await cli.run({
arguments: ["inspect-electron-fuses", app, "--json"],
});
expect(result.exitCode, result.stderr).toBe(0);
const evidence = toolContract("inspect_electron_fuses").outputSchema.parse(
result.json,
);
expect(evidence.normalized_result.artifact).toMatchObject({
selected_path: app,
resolved_path: binary,
format: "mach-o",
});
expect(evidence.subject?.local_path).toBe(binary);
},
);
cliTest(
"rejects an unrecognized container through the public CLI",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-electron-fuses-cli-text-");
const path = join(root, "notes.txt");
await writeFile(path, "This is not an executable container.");
const result = await cli.run({
arguments: ["inspect-electron-fuses", path, "--json"],
});
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({
code: "unsupported_target",
details: { operation: "inspect_electron_fuses", path },
});
},
);

View File

@ -0,0 +1,180 @@
import type { Client } from "@modelcontextprotocol/client";
import { Ajv2020 } from "ajv/dist/2020.js";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { expandToolAvailability } from "../../../src/contracts/toolOutputSchemaPrimitives.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { createServer } from "../../../src/server/createServer.js";
import { ELECTRON_FUSE_STATE_BYTES } from "../../../src/domain/electron/electronFuses.js";
import {
elfFuseImage,
truncatedFuseImage,
twoSentinelElf,
} from "../../fixtures/electron/fuseImage.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { mcpTest } from "../../support/mcp/mcpFixture.js";
const fuseMcpTest = mcpTest.extend<{
fuse: { client: Client };
}>({
fuse: async ({ mcp }, use) => {
const session = createTestBinarySession(() => {
throw new Error(
"Electron fuse inspection must not start a deep provider",
);
});
try {
const client = await mcp.connect(
createServer({ kind: "session", session }),
);
await use({ client });
} finally {
await session.close();
}
},
});
fuseMcpTest(
"advertises fuse inspection and preserves unknown fuse bytes through MCP",
async ({ fuse: { client }, mcp }) => {
const session = createTestBinarySession(() => {
throw new Error(
"Electron fuse inspection must not start a deep provider",
);
});
try {
const connected = await mcp.connect(
createServer({ kind: "session", session }),
);
const status = await connected.callTool({
name: "binary_session",
arguments: {},
});
const availability = expandToolAvailability(
toolContract("binary_session").outputSchema.parse(
status.structuredContent,
).result.tool_availability,
);
expect(availability).toContainEqual(
expect.objectContaining({
name: "inspect_electron_fuses",
available: true,
reason: "available",
}),
);
const advertised = (await connected.listTools()).tools.find(
(tool) => tool.name === "inspect_electron_fuses",
);
if (advertised?.outputSchema === undefined)
throw new Error("Electron fuse inspection must publish both schemas");
const ajv = new Ajv2020({ strict: false, validateFormats: false });
const inputSchema: Record<string, unknown> = advertised.inputSchema;
const outputSchema: Record<string, unknown> = advertised.outputSchema;
expect(ajv.validateSchema(inputSchema)).toBe(true);
expect(ajv.validateSchema(outputSchema)).toBe(true);
expect(advertised.annotations).toMatchObject({
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
});
const root = await createTestTempDirectory("rea-electron-fuses-mcp-");
const elf = elfFuseImage([ELECTRON_FUSE_STATE_BYTES.removed, 0x99]);
const path = join(root, "application");
await writeFile(path, elf.bytes);
const response = await connected.callTool({
name: "inspect_electron_fuses",
arguments: { path },
});
expect(response.isError, JSON.stringify(response.content)).not.toBe(true);
expect(
ajv.validate(outputSchema, response.structuredContent),
JSON.stringify(ajv.errors),
).toBe(true);
const parsed = toolContract("inspect_electron_fuses").outputSchema.parse(
response.structuredContent,
);
const evidence = parseEvidence(parsed);
expect(evidence.confidence).toBe("observed");
expect(parsed.normalized_result.artifact.format).toBe("elf");
expect(
parsed.normalized_result.slices[0]?.sentinels[0]?.fuses,
).toMatchObject([
{ index: 0, name: "RunAsNode", state: "removed", raw_byte: 0x72 },
{
index: 1,
name: "EnableCookieEncryption",
state: "unknown",
raw_byte: 0x99,
},
]);
expect(parsed.normalized_result.runtime_implications).toEqual([
expect.objectContaining({
subject: "electron_run_as_node",
relationship: "derivation",
outcome: "not_honored",
fuse_index: 0,
}),
]);
expect(session.evidenceById(evidence.evidence_id)).toEqual(evidence);
const duplicated = twoSentinelElf();
const duplicatePath = join(root, "duplicated");
await writeFile(duplicatePath, duplicated.bytes);
const duplicates = await connected.callTool({
name: "inspect_electron_fuses",
arguments: { path: duplicatePath },
});
expect(duplicates.isError).not.toBe(true);
const duplicateEvidence = toolContract(
"inspect_electron_fuses",
).outputSchema.parse(duplicates.structuredContent);
expect(
duplicateEvidence.normalized_result.slices[0]?.sentinels.map(
(item) => item.sentinel_offset,
),
).toEqual([...duplicated.offsets]);
const truncatedPath = join(root, "truncated");
await writeFile(truncatedPath, truncatedFuseImage());
const truncated = await connected.callTool({
name: "inspect_electron_fuses",
arguments: { path: truncatedPath },
});
expect(truncated.isError).not.toBe(true);
const truncatedEvidence = toolContract(
"inspect_electron_fuses",
).outputSchema.parse(truncated.structuredContent);
expect(
truncatedEvidence.normalized_result.slices[0]?.sentinels[0],
).toMatchObject({
complete: false,
declared_length: 4,
wire_version: 1,
});
const relative = await client.callTool({
name: "inspect_electron_fuses",
arguments: { path: "relative.bin" },
});
expect(relative.isError).toBe(true);
expect(parseMcpToolError(relative)).toMatchObject({
error: {
code: "invalid_request",
details: {
operation: "inspect_electron_fuses",
issues: [expect.objectContaining({ path: ["path"] })],
},
},
});
} finally {
await session.close();
}
},
);

View File

@ -0,0 +1,199 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "vitest";
import { z } from "zod";
import { fileURLToPath } from "node:url";
type Delivery = "complete" | "summary";
type Observation = {
evidence_id: string;
parent_evidence_id?: string;
limitations: string[];
kind?: string;
view?: { kind: string; facet?: string; offset?: number; limit?: number };
coverage?:
| string
| {
examined: number;
total: number;
next_offset: number | null;
exhausted: boolean;
};
unknowns?: string[];
};
type Fixture = {
summary: {
artifact: string;
delivery: Delivery;
provider_search_path_sha256: string;
calls: unknown[];
};
answer: {
capture: { coverage: string; capture_start: null };
http_request_uris_first2: string[];
delivery_observations: Observation[];
};
};
const reportSchema = z.object({
facts_match: z.boolean(),
settings_match: z.boolean(),
fact_differences: z.array(z.string()),
preserved_run_specific_values: z.array(z.object({ value: z.unknown() })),
});
// Synthetic measurement directories exercise the comparator CLI, not providers.
const observation = (kind: string): Observation => ({
evidence_id: `ev_${kind}`,
parent_evidence_id: "ev_parent",
limitations: ["saved bytes only"],
kind,
view:
kind === "summary"
? { kind }
: { kind, facet: "http-uris", offset: 0, limit: 2 },
coverage: { examined: 2, total: 3, next_offset: 2, exhausted: false },
unknowns: ["unrecorded traffic"],
});
const fixture = (delivery: Delivery, artifact: string): Fixture => ({
summary: {
artifact,
delivery,
provider_search_path_sha256: "a".repeat(64),
calls: [],
},
answer: {
capture: { coverage: "complete", capture_start: null },
http_request_uris_first2: [
"http://example.test/one",
"http://example.test/two",
],
delivery_observations:
delivery === "summary"
? [observation("summary"), observation("pcap")]
: [
{
evidence_id: "ev_complete",
limitations: ["saved bytes only"],
coverage: "complete",
},
],
},
});
function compare(left: Fixture, right: Fixture) {
const root = mkdtempSync(join(tmpdir(), "rea-compare-test-"));
try {
const directories = [join(root, "left"), join(root, "right")] as const;
for (const [index, value] of [left, right].entries()) {
const directory = directories[index];
assert.ok(directory);
mkdirSync(directory);
for (const name of ["summary", "answer"] as const)
writeFileSync(
join(directory, `${name}.json`),
JSON.stringify(value[name]),
);
}
const result = spawnSync(
process.execPath,
[
fileURLToPath(
new URL(
"../../experiments/analysis-efficiency/compare.mjs",
import.meta.url,
),
),
"--left",
directories[0],
"--right",
directories[1],
],
{ encoding: "utf8" },
);
assert.ifError(result.error);
assert.equal(result.stderr, "");
return {
status: result.status,
report: reportSchema.parse(JSON.parse(result.stdout)),
};
} finally {
rmSync(root, { recursive: true, force: true });
}
}
for (const artifact of [
"wireshark-http.cap",
"wireshark-http_redirects.pcapng",
]) {
test(`${artifact}: repeated modes ignore only receipt IDs`, () => {
for (const delivery of ["complete", "summary"] as const) {
const left = fixture(delivery, artifact);
const right = structuredClone(left);
for (const value of right.answer.delivery_observations) {
value.evidence_id += "_second";
if (typeof value.parent_evidence_id === "string")
value.parent_evidence_id += "_second";
}
const { status, report } = compare(left, right);
assert.equal(status, 0);
assert.equal(report.facts_match, true);
assert.deepEqual(
report.preserved_run_specific_values.map((value) => value.value),
[left.answer.delivery_observations, right.answer.delivery_observations],
);
}
});
test(`${artifact}: repeated modes reject stable observation changes`, () => {
for (const delivery of ["complete", "summary"] as const) {
const left = fixture(delivery, artifact);
const mutations: ((values: Observation[]) => void)[] = [
(values) => values[0]!.limitations.push("new limitation"),
(values) => {
values[0]!.coverage = "partial";
},
(values) => {
delete values[0]!.coverage;
},
];
if (delivery === "summary")
mutations.push(
(values) => {
values[1]!.view!.offset = 1;
},
(values) => {
const coverage = values[1]!.coverage;
assert.ok(typeof coverage === "object" && coverage !== null);
coverage.next_offset = null;
},
(values) => values[1]!.unknowns!.push("extra unknown"),
(values) => values.reverse(),
);
for (const mutate of mutations) {
const right = structuredClone(left);
mutate(right.answer.delivery_observations);
const { status, report } = compare(left, right);
assert.equal(status, 1);
assert.equal(report.settings_match, true);
assert.equal(report.facts_match, false);
assert.deepEqual(report.fact_differences, ["delivery_observations"]);
}
}
});
test(`${artifact}: different modes preserve their different receipt scopes`, () => {
const left = fixture("complete", artifact);
const right = fixture("summary", artifact);
const { status, report } = compare(left, right);
assert.equal(status, 0);
assert.equal(report.facts_match, true);
assert.deepEqual(
report.preserved_run_specific_values.map((value) => value.value),
[left.answer.delivery_observations, right.answer.delivery_observations],
);
right.answer.capture.coverage = "partial";
assert.equal(compare(left, right).report.facts_match, false);
});
}

146
tests/fixtures/electron/fuseImage.ts vendored Normal file
View File

@ -0,0 +1,146 @@
import {
ELECTRON_FUSE_SENTINEL,
ELECTRON_FUSE_STATE_BYTES,
} from "../../../src/domain/electron/electronFuses.js";
export const FUSE_SENTINEL = Buffer.from(ELECTRON_FUSE_SENTINEL, "ascii");
/** Obsidian's observed version-1 states: disabled, enabled, then six more with the last enabled. */
export const OBSIDIAN_FUSE_BYTES = [
ELECTRON_FUSE_STATE_BYTES.disabled,
ELECTRON_FUSE_STATE_BYTES.enabled,
ELECTRON_FUSE_STATE_BYTES.disabled,
ELECTRON_FUSE_STATE_BYTES.disabled,
ELECTRON_FUSE_STATE_BYTES.disabled,
ELECTRON_FUSE_STATE_BYTES.disabled,
ELECTRON_FUSE_STATE_BYTES.disabled,
ELECTRON_FUSE_STATE_BYTES.enabled,
] as const;
export const fuseWire = (version: number, fuses: readonly number[]): Buffer =>
Buffer.from([version, fuses.length, ...fuses]);
export const thinMacho = (cpuType: number, body: Buffer): Buffer => {
const header = Buffer.alloc(32);
header.writeUInt32LE(0xfeedfacf, 0);
header.writeUInt32LE(cpuType, 4);
header.writeUInt32LE(0, 8);
header.writeUInt32LE(2, 12);
return Buffer.concat([header, body]);
};
export const thinFuseImage = (
fuses: readonly number[],
version = 1,
): { readonly bytes: Buffer; readonly sentinelOffset: number } => ({
bytes: thinMacho(
0x0100000c,
Buffer.concat([FUSE_SENTINEL, fuseWire(version, fuses)]),
),
sentinelOffset: 32,
});
export const universalFuseImage = (): {
readonly bytes: Buffer;
readonly left: {
readonly fileOffset: number;
readonly sentinelOffset: number;
};
readonly right: {
readonly fileOffset: number;
readonly sentinelOffset: number;
};
} => {
const left = thinMacho(
0x01000007,
Buffer.concat([FUSE_SENTINEL, fuseWire(1, OBSIDIAN_FUSE_BYTES)]),
);
const rightFuses = [...OBSIDIAN_FUSE_BYTES];
rightFuses[3] = ELECTRON_FUSE_STATE_BYTES.enabled;
const right = thinMacho(
0x0100000c,
Buffer.concat([FUSE_SENTINEL, fuseWire(1, rightFuses)]),
);
const tableEnd = 48;
const table = Buffer.alloc(tableEnd);
table.writeUInt32BE(0xcafebabe, 0);
table.writeUInt32BE(2, 4);
const writeEntry = (
index: number,
cpuType: number,
offset: number,
size: number,
) => {
const base = 8 + index * 20;
table.writeUInt32BE(cpuType, base);
table.writeUInt32BE(0, base + 4);
table.writeUInt32BE(offset, base + 8);
table.writeUInt32BE(size, base + 12);
table.writeUInt32BE(0, base + 16);
};
writeEntry(0, 0x01000007, tableEnd, left.length);
writeEntry(1, 0x0100000c, tableEnd + left.length, right.length);
return {
bytes: Buffer.concat([table, left, right]),
left: { fileOffset: tableEnd, sentinelOffset: tableEnd + 32 },
right: {
fileOffset: tableEnd + left.length,
sentinelOffset: tableEnd + left.length + 32,
},
};
};
export const peFuseImage = (
fuses: readonly number[],
): { readonly bytes: Buffer; readonly sentinelOffset: number } => {
const header = Buffer.alloc(0x80);
header.write("MZ", 0, "ascii");
header.writeUInt32LE(0x80, 0x3c);
const body = Buffer.concat([
Buffer.from("PE\0\0", "binary"),
FUSE_SENTINEL,
fuseWire(1, fuses),
]);
return { bytes: Buffer.concat([header, body]), sentinelOffset: 0x84 };
};
export const elfFuseImage = (
fuses: readonly number[],
version = 1,
): { readonly bytes: Buffer; readonly sentinelOffset: number } => ({
bytes: Buffer.concat([
Buffer.from([0x7f, 0x45, 0x4c, 0x46]),
FUSE_SENTINEL,
fuseWire(version, fuses),
]),
sentinelOffset: 4,
});
export const truncatedFuseImage = (): Buffer =>
thinMacho(
0x0100000c,
Buffer.concat([FUSE_SENTINEL, Buffer.from([1, 4, 0x31])]),
);
export const twoSentinelElf = (): {
readonly bytes: Buffer;
readonly offsets: readonly [number, number];
} => {
const wire = fuseWire(1, OBSIDIAN_FUSE_BYTES);
const first = 4;
const second = first + FUSE_SENTINEL.length + wire.length + 2;
return {
bytes: Buffer.concat([
Buffer.from([0x7f, 0x45, 0x4c, 0x46]),
FUSE_SENTINEL,
wire,
Buffer.from([0, 0]),
FUSE_SENTINEL,
wire,
]),
offsets: [first, second],
};
};
export const sentinelFreeMacho = (): Buffer =>
thinMacho(0x0100000c, Buffer.from("no fuse wire in this image"));