feat(electron): add autonomous Electron runtime analysis (#464)

* feat: add autonomous Electron runtime analysis

* feat(electron): add lifecycle and boundary telemetry

* feat(electron): capture runtime boundary evidence

* fix(electron): package boundary hook helper

* feat(electron): report active capture coverage

* fix(electron): account for bring-your-own runtime

* feat(electron): expand agent-owned runtime scenarios

* fix(ghidra): keep fixture credentials out of argv

* perf(test): serialize local test and docs workloads

* fix(electron): harden active runtime capture

* fix(inspector): require exact runtime target authorization

* fix(docs): make managed manifest host independent

* fix(ci): make generated catalog portable

* fix: unblock autonomous command and electron flows

* fix(electron): close popup and detached helper gaps
This commit is contained in:
morluto 2026-08-05 19:07:46 +08:00 committed by GitHub
parent 51559245d2
commit 8f229ede70
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
104 changed files with 5055 additions and 196 deletions

View File

@ -204,6 +204,17 @@ jobs:
if-no-files-found: error
retention-days: 30
conformance-fixtures:
name: Source-built conformance fixtures
needs: changes
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- name: Verify fixtures from a clean checkout
run: npm run verify:fixtures
package-e2e:
name: Package E2E (${{ matrix.os }})
needs: changes
@ -268,7 +279,7 @@ jobs:
run: npm run verify:inspector
windows-curated:
name: Windows Ghidra P0 and package lane
name: Windows capability and package lane
needs: changes
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
runs-on: windows-latest
@ -289,7 +300,7 @@ jobs:
node -e "const report = JSON.parse(require('node:fs').readFileSync('windows-capabilities.json', 'utf8'));
if (report.platform !== 'win32' || report.architecture !== 'x64')
throw new Error('expected a Windows x64 capability report');"
- name: Run curated Windows P0 tests
- name: Run curated Windows capability tests
run: >-
npx vitest run --coverage.enabled=false
tests/composition/analysis-sessions/binaryTarget.test.ts
@ -299,7 +310,6 @@ jobs:
tests/boundary/providers/ghidra/ghidraProvider.test.ts
tests/boundary/providers/ghidra/ghidraTransport.test.ts
tests/boundary/providers/ghidra/ghidraTargetSnapshot.test.ts
tests/boundary/providers/ghidra/ghidraClient.test.ts
tests/boundary/providers/ghidra/ghidraLauncher.test.ts
tests/boundary/process/processOwnership.test.ts
tests/boundary/process/processOwnershipPart2.test.ts

View File

@ -132,3 +132,41 @@ jobs:
done
echo "Published rea-agents@${version} failed registry and CLI verification" >&2
exit 1
publish-mcp:
name: Publish to MCP Registry
needs: [release-please, publish]
if: needs.release-please.outputs.release_created == 'true' && needs.publish.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.release-please.outputs.tag_name }}
persist-credentials: false
- name: Verify package and registry metadata versions
run: npm run mcp-registry:check
- name: Install MCP Registry publisher
env:
MCP_PUBLISHER_VERSION: v1.8.0
MCP_PUBLISHER_SHA256_LINUX_AMD64: 1370446bbe74d562608e8005a6ccce02d146a661fbd78674e11cc70b9618d6cf
run: |
archive="mcp-publisher_linux_amd64.tar.gz"
curl -fsSLo "${archive}" \
"https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}/${archive}"
echo "${MCP_PUBLISHER_SHA256_LINUX_AMD64} ${archive}" | sha256sum --check --strict
tar xzf "${archive}" mcp-publisher
chmod +x mcp-publisher
- name: Validate MCP Registry metadata
run: ./mcp-publisher validate
- name: Authenticate to MCP Registry
run: ./mcp-publisher login github-oidc
- name: Publish MCP Registry metadata
run: ./mcp-publisher publish

View File

@ -54,6 +54,7 @@ See [docs/architecture.mermaid](docs/architecture.mermaid) for a visual architec
- `npm run build`: compile `src/` into `dist/`.
- `npm run build:cached`: compile through Turbo and reuse a valid local build cache entry.
- `npm test`: restore or build `dist/`, then run the Vitest suite once without coverage or retries locally.
- `npm run test:local`: run only changed and related tests without rebuilding `dist/`; use this for the lightweight local loop.
- `npm run test:fast`: run pure and subprocess test projects without the serial integration group.
- `npm run test:changed`: run changed and related non-serial tests once.
- `npm run test:integration`: run the serial filesystem, process, and CLI integration group.
@ -85,6 +86,14 @@ See [docs/architecture.mermaid](docs/architecture.mermaid) for a visual architec
- `npm run docs:api:cached`: render API documentation through Turbo or restore a valid local result.
- `npm run docs:generate`: refresh committed metadata and render the uncommitted API documentation.
- `npm run docs:check`: verify generated package metadata, the canonical product catalog, caller-visible documentation facts, TypeDoc rendering, and the error JSON schema.
Local full-suite Vitest runs use one worker and serial project groups because
boundary tests own subprocess fixtures. CI retains the existing two-worker
budget. The `test`, `docs:check`, and `docs:generate` commands use
repository-local locks and refuse overlapping runs; interrupted commands can
leave a stale lock that is removed automatically when its owner PID is no
longer alive. The `npm test` build runs inside that lock.
- `npm run config:print -- /path/to/binary`: print an MCP server config with absolute paths.
- `HOPPER_TARGET_PATH=/path/to/binary npm start`: launch Hopper and run the built stdio MCP server.

View File

@ -386,7 +386,7 @@ REA handles the app analysis in steps 1–5. The agent performs step 6 with its
| Artifact graph | 3 | bounded provider-neutral inspection, deterministic directory/ZIP/APK/IPA/MSIX/AppX/ASAR inventory, and explicitly selected extraction into an absent owned tree |
| Managed PE/CLI | 8 | PE/CLI identity, metadata members, CIL hashes, P/Invoke/native-boundary declarations and verification, application-graph projection, decompiler reconstruction import, token remapping, runtime-correlation plans, and version comparison |
| Browser observation | 9 | exact-origin passive CDP capture, bundle and source-map analysis, WebMCP discovery, session timelines, capture diff, visual evidence, and bounded Playwright scenarios |
| Electron analysis | 4 | passive root-confined observation, bounded static application mapping, and evidence-backed static/runtime reconciliation |
| Electron analysis | 5 | passive root-confined observation, bounded static application mapping, evidence-backed static/runtime reconciliation, and separately approved provider-owned click/wait scenarios |
| JavaScript runtime | 2 | approved attach-only Node/Electron Inspector target discovery plus bounded script and execution-context observation without evaluation or instrumentation |
| Application workflows | 12 | bounded cross-layer traces, unique-only version matching, historical-source to bundle mapping, static export return-shape comparison, approved Linux-isolated extracted-module replay, managed-runtime characterization, reconstruction coverage closure, deterministic obligation ledgers, and end-to-end readiness conformance |
| Workspace and observation | 23 | target lifecycle, Evidence v2 bundle snapshots, retained-bundle release, aggregate navigation/address context, direct finite replay-machine evaluation, process/artifact/function comparison, evidence-linked residual-unknown lifecycle |
@ -509,14 +509,14 @@ REA is growing into a toolkit for understanding software across static artifacts
### Next
1. **Controlled replay conformance growth** — extend the shipped Linux extracted-module sandbox with more source-owned hostile fixtures and cross-kernel conformance; browser or Electron interaction remains a different future authority.
1. **Controlled replay conformance growth** — extend the shipped Linux extracted-module sandbox with more source-owned hostile fixtures and cross-kernel conformance; browser and Electron scenarios remain separately permissioned authorities.
2. **Broader application graph evidence** — extend authenticated cross-layer traces with additional static extractors and separately approved runtime authorities.
3. **Professional managed-code analysis** — extend shipped PE/CLI triage, CIL evidence, managed/native declaration inventory, source-owned conformance, and obfuscation-resistant comparisons toward verified native-provider composition under the accepted [managed-code boundary](docs/managed-code-analysis.md).
4. **Deterministic behavior harnesses** — extend process ownership, protocol fixtures, filesystem observation, reconnects, and cross-version behavioral comparison.
### Later
1. **Controlled application interaction** — evaluate separately authorized full-application driving without widening passive browser, Electron, or extracted-module replay authority.
1. **Broader controlled application interaction** — extend the separately authorized browser and Electron scenario surfaces beyond the current bounded click/wait actions without widening passive observation or extracted-module replay authority.
2. **Native runtime observation** — approval-gated LLDB, Frida, system logs, process/filesystem observers, and native API tracing.
3. **Additional providers and targets** — evaluate IDA/Hex-Rays, Binary Ninja, Rizin, LIEF, Windows-native providers, mobile artifacts, firmware, document formats, and other software-defined systems.

View File

@ -167,7 +167,7 @@ rea uninstall --purge-data
| رسم القطع الأثرية | 3 | جرد حتمي للدلائل وZIP/APK/IPA وASAR واستخراج معاملاتي محدد صراحة |
| Managed PE/CLI | 8 | هوية PE/CLI، أعضاء البيانات الوصفية، بصمات CIL، تصريحات P/Invoke/الحدود الأصلية والتحقق منها، إسقاط رسم التطبيق، استيراد إعادة البناء من فك الترجمة، إعادة ربط الرموز، خطط ترابط وقت التشغيل ومقارنة الإصدارات |
| مراقبة المتصفح | 9 | التقاط CDP محدود الأصل وتحليل الحزم وخرائط المصدر واكتشاف WebMCP والفروق والأدلة المرئية |
| تحليل Electron | 4 | مراقبة سلبية ضمن جذور canonical، ورسم ثابت ومحدود للتطبيق، ومطابقة ثابتة/وقت تشغيل قائمة على Evidence |
| تحليل Electron | 5 | مراقبة سلبية ضمن جذور canonical، ورسم ثابت ومحدود للتطبيق، ومطابقة ثابتة/وقت تشغيل قائمة على Evidence |
| وقت تشغيل JavaScript | 2 | اكتشاف أهداف Node/Electron Inspector المعتمدة للاتصال فقط، ومراقبة محدودة للبرامج النصية وسياقات التنفيذ دون تقييم أو تجهيز |
| سير عمل التطبيقات | 12 | تتبع محدود ومقارنة إصدارات فريدة ومقارنة ثابتة لأشكال قيم إرجاع الصادرات وإعادة تشغيل وحدات معزولة ومعتمدة على Linux وتوصيف وقت التشغيل المُدار وإغلاق تغطية إعادة البناء ودفاتر التزامات حتمية والتحقق الشامل من جاهزية إعادة البناء |
| جلسة الملف التنفيذي | 23 | فتح الأهداف وحزم الأدلة وتحرير الحزم المحتفظ بها وتشغيل آلة إعادة محدودة ومقارنة العمليات والقطع والدوال وسجل المجهولات المتبقية |

View File

@ -157,7 +157,7 @@ REA は手順 1〜5 のバイナリ解析を処理し、手順 6 はエージェ
| アーティファクトグラフ | 3 | ディレクトリ、ZIP/APK/IPA、ASAR の決定的一覧と明示選択されたトランザクション抽出 |
| Managed PE/CLI | 8 | PE/CLI 識別、メタデータメンバー、CIL ハッシュ、P/Invoke/ネイティブ境界宣言と検証、アプリケーショングラフ投影、逆コンパイル再構成インポート、構造的 token 再マッピング、ランタイム相関計画、バージョン比較 |
| ブラウザ観察 | 9 | origin 限定 CDP 取得、bundle/source map 解析、WebMCP 検出、セッション、capture diff、視覚証拠 |
| Electron 解析 | 4 | canonical ファイルルート内の受動観察、有界な静的アプリマッピング、Evidence に基づく静的/実行時の照合 |
| Electron 解析 | 5 | canonical ファイルルート内の受動観察、有界な静的アプリマッピング、Evidence に基づく静的/実行時の照合 |
| JavaScript ランタイム | 2 | 承認済みのアタッチ専用 Node/Electron Inspector ターゲット検出と、評価や計装を行わない有界なスクリプト/実行コンテキスト観察 |
| アプリケーション | 12 | 有界なクロスレイヤー追跡、一意限定のバージョン比較、静的なエクスポート戻り値形状比較、承認済み Linux 隔離 extracted-module replay、マネージドランタイム特性評価、再構築カバレッジ閉包、決定論的な義務台帳、エンドツーエンドの準備適合性検証 |
| バイナリセッション | 23 | `open_binary`、`binary_session`、証拠バンドル、保持バンドルの解放、有限リプレイ状態機械、プロセス/成果物/関数比較、残余未知レジストリ |

View File

@ -157,7 +157,7 @@ REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이
| 아티팩트 그래프 | 3 | 디렉터리, ZIP/APK/IPA, ASAR 결정적 인벤토리와 명시적으로 선택한 트랜잭션 추출 |
| Managed PE/CLI | 8 | PE/CLI ID, 메타데이터 멤버, CIL 해시, P/Invoke/네이티브 경계 선언 및 검증, 애플리케이션 그래프 투영, 디컴파일 재구성 가져오기, 구조적 token 재매핑, 런타임 상관 계획, 버전 비교 |
| 브라우저 관찰 | 9 | origin 제한 CDP 캡처, bundle/source map 분석, WebMCP 검색, 세션 타임라인, capture diff 및 시각 증거 |
| Electron 분석 | 4 | canonical 파일 루트 내 수동 관찰, 제한된 정적 앱 매핑, Evidence 기반 정적/런타임 조정 |
| Electron 분석 | 5 | canonical 파일 루트 내 수동 관찰, 제한된 정적 앱 매핑, Evidence 기반 정적/런타임 조정 |
| JavaScript 런타임 | 2 | 승인된 연결 전용 Node/Electron Inspector 대상 탐색과 평가·계측 없는 제한된 스크립트 및 실행 컨텍스트 관찰 |
| 애플리케이션 | 12 | 제한된 교차 계층 추적, 고유 일치 버전 비교, 정적 export 반환 형태 비교, 승인된 Linux 격리 extracted-module replay, 관리형 런타임 특성화, 재구성 커버리지 폐쇄, 결정론적 의무 원장, 엔드투엔드 준비 적합성 검증 |
| 바이너리 세션 | 23 | `open_binary`, `binary_session`, 증거 번들, 보존 번들 해제, 유한 replay 상태 머신, 프로세스·아티팩트·함수 비교, 잔여 미지수 레지스트리 |

View File

@ -157,7 +157,7 @@ REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常
| 制品图谱 | 3 | 确定性目录、ZIP/APK/IPA 与 ASAR 清单,以及显式选择的事务式提取 |
| Managed PE/CLI | 8 | PE/CLI 身份、元数据成员、CIL 哈希、P/Invoke/原生边界声明与验证、应用图谱投影、反编译重建导入、结构化 token 重映射、运行时关联计划与跨版本比较 |
| 浏览器观察 | 9 | 精确来源 CDP 抓取、bundle/source map 分析、WebMCP 发现、会话时间线、capture diff 与视觉证据 |
| Electron 分析 | 4 | 限定 canonical 文件根的被动观察、有界静态应用映射,以及基于 Evidence 的静态/运行时对账 |
| Electron 分析 | 5 | 限定 canonical 文件根的被动观察、有界静态应用映射,以及基于 Evidence 的静态/运行时对账 |
| JavaScript 运行时 | 2 | 经批准的只附加 Node/Electron Inspector 目标发现,以及不执行求值或插桩的有界脚本和执行上下文观察 |
| 应用工作流 | 12 | 有界跨层追踪、唯一匹配跨版本图比较、静态导出返回形状比较、经批准的 Linux 隔离 extracted-module replay、托管运行时表征、重建覆盖闭合、确定性义务账本和端到端就绪性一致性验证 |
| 二进制会话 | 23 | `open_binary`、`binary_session`、证据包、保留包释放、有限重放状态机、进程、制品与函数比较、残余未知项注册表 |

View File

@ -57,3 +57,55 @@ work completed.
Inspection does not retain DOM values, execute renderer code, navigate, click,
invoke Electron IPC, close a target, or terminate the application.
## Active Electron scenarios
Active Electron authority is separate and disabled by default. When enabled,
REA launches an operator-approved Electron executable through the official
Playwright Electron API, owns its lifetime, accepts bounded click/wait actions,
window-targeted renderer reload/crash actions, and synthetic `open-url` or
`second-instance` deep-link delivery. It records capture-scoped window and
WebContents identities, process metrics, and IPC channel and value-shape metadata.
Payload values are not retained.
Configure exact roots and run the real fixture verifier with an operator-owned
Electron runtime:
```bash
export REA_ELECTRON_AUTOMATE_ENABLED=true
export REA_ELECTRON_AUTOMATE_AUTO_GRANT=false
export REA_ELECTRON_AUTOMATE_EXECUTABLE_ROOTS_JSON='["/absolute/path/to/runtime"]'
export REA_ELECTRON_AUTOMATE_APPLICATION_ROOTS_JSON='["/absolute/path/to/app"]'
REA_ELECTRON_EXECUTABLE=/absolute/path/to/electron npm run verify:electron
```
This capability actively launches and interacts with the target. It is not a
passive CDP observation and must be granted separately as `electron_automate`.
The default is fail-closed (`AUTO_GRANT=false`); an operator can instead issue
a project/session/one-shot grant through the normal permission workflow. The
owned process keeps normal host filesystem and network privileges, so this is
an authority boundary and lifecycle boundary, not a sandbox.
The CLI and MCP surfaces accept the same schema. For a JSON request file:
```bash
rea capture-electron-scenario scenario.json --json
```
The result records bounded action status and targets, correlated app/window/WebContents,
preload, session, navigation, shell, permission, popup, download, protocol,
native-addon, process, and IPC timeline events. IPC channels are capped at
1,024 characters and argument-shape metadata at 32 entries; values are never
retained. Renderer crash/restart and deep-link actions are synthetic, bounded
scenario controls; their attempted and observed outcomes remain in the
timeline. The active hook blocks and records external shell/navigation,
permission, download, popup, updater, and OS-integration effects. The timeline
is explicitly partial when attachment starts after application activity or when
the event budget is exhausted.
Active Electron Evidence can also be supplied to
[`reconcile_javascript_runtime`](javascript-runtime-reconciliation.md). That
projection is intentionally target-only and partial: it binds the approved
application path and capture outcome to the static graph, while frames, scripts,
workers, and execution claims remain unavailable until a separate passive runtime
capture provides them.

View File

@ -259,3 +259,30 @@ target/snapshot/import digest identity, and requires complete runtime cleanup.
## Diagnose, update, and remove
`rea doctor --json` is strictly read-only. `rea upgrade` updates only the npm installation that owns the running CLI. `rea uninstall` removes only REA-owned agent registrations and skill files; `--purge-data` additionally removes REA cache and state paths.
## MCP Registry
REA is published in the official MCP Registry as `io.github.morluto/rea`. Registry
clients can discover the server and install the existing public `rea-agents` npm
package; the Registry entry does not introduce a second distribution artifact.
For a client that supports Registry discovery, search for `io.github.morluto/rea`
and select the npm package. The published metadata launches the existing stdio
server through `npx` with the `mcp` command.
For a client that requires manual configuration, use:
```json
{
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@latest", "mcp"]
}
}
}
```
Use an exact `rea-agents@VERSION` in the arguments when a reproducible client
configuration is required. `rea setup` writes the same package-runner shape and
pins it to the exact version that performed setup.

View File

@ -47,9 +47,11 @@ REA offers the once-or-session grant form only after the connection negotiates
an MCP revision with multi-round tool results and advertises form elicitation.
Older connections keep returning the structured
permission-required result and never wait for an unsupported elicitation
response. The MCP SDK runtime currently pinned by REA negotiates the older
protocol path, so the interactive flow remains fail-closed until its runtime
protocol support is available.
response. On the pinned `@modelcontextprotocol/server` 2.0.0 runtime, this is
the official `2026-07-28` `inputRequired` continuation: the SDK seals the
request state and validates the continuation envelope, while REA validates the
untrusted input response before creating a connection-owned grant. The older
push-style `elicitation/create` path is not used for this protocol revision.
Every scenario must also contain `"approved": true`. Executables, working
directories, filesystem roots, and requested environment variables are checked

View File

@ -11,7 +11,7 @@
"skill_version": "23"
},
"tools": {
"total": 115,
"total": 116,
"families": [
{
"id": "direct",
@ -128,9 +128,10 @@
{
"id": "electron",
"surface": "electron-provider",
"count": 4,
"count": 5,
"tools": [
"analyze_javascript_application",
"capture_electron_scenario",
"inspect_electron_page",
"list_electron_targets",
"reconcile_javascript_runtime"
@ -369,6 +370,12 @@
"version": "1",
"capabilities": ["capture_browser_scenario"]
},
{
"id": "rea-playwright-electron-active",
"name": "REA Playwright active Electron observation provider",
"version": "1",
"capabilities": ["capture_electron_scenario"]
},
{
"id": "rea-v8-inspector",
"name": "REA passive Node/Electron V8 Inspector provider",
@ -586,7 +593,7 @@
}
],
"cli": {
"primary_count": 67,
"primary_count": 68,
"commands": [
"analyze",
"analyze-javascript-application",
@ -594,6 +601,7 @@
"build-reconstruction-obligation-ledger",
"capabilities",
"capture-browser-scenario",
"capture-electron-scenario",
"capture-process",
"capture-web-screenshot",
"commit-reconstruction-coverage",
@ -665,18 +673,18 @@
},
"runtime_catalog": {
"counts": {
"cli_commands": 67,
"mcp_tools": 115,
"cli_commands": 68,
"mcp_tools": 116,
"mcp_prompts": 6,
"mcp_resources": 2,
"mcp_resource_templates": 11
},
"digests": {
"tools_sha256": "4623a3b42560c4aaea746e16a4c1feab7714b479f43401b2e8e1ad0bbb1b4746",
"tools_sha256": "8021786c901ca9225ba8e2477f220c8c15d61b8df5618b221f61417cae8c8a36",
"prompts_sha256": "3886a3bf55a448def79e3eb429e6a56eb2ba075423c5cb1e5747642b389ef8fe",
"resources_sha256": "e0e872276ebf6f88e916ef24930a99eed27ce4c93e494e60820e9bd8cc8497c3",
"combined_sha256": "6b2470559f830c70f2b1a1453fd8cf1264f690b6f6718342deb04406aefa9fe1",
"providers_sha256": "918ccd6b8ae442cd5844dcfde55086f458176241c8d166c1e9dd0f242ccd2bb0"
"combined_sha256": "2da444fd95a40c28a6c0332a5970edca6b984f0c2922e4c02eaf7458a6f3dd0e",
"providers_sha256": "7446b51788734ebf6f4f702e3a5a8c9f0b61967490c245f30bd37b2060827e6b"
}
}
}

View File

@ -46,6 +46,7 @@ are:
| Command | Scope |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `npm run test:fast` | Domain, service, adapter, composition, boundary, MCP boundary, and conformance projects |
| `npm run test:local` | Changed and related tests without the build step; lightweight local feedback loop |
| `npm run test:boundary` | Boundary, MCP boundary, and process-global projects |
| `npm run test:mcp` | MCP boundary project only |
| `npm run test:acceptance` | Complete CLI and MCP acceptance workflows |
@ -60,6 +61,15 @@ miss behavior connected through runtime registration, generated data, shell
entrypoints, or other relationships that are absent from the import graph.
Use `npm run check:pr` before handing off a contribution.
Local full-suite Vitest runs are intentionally capped at one worker and one
project at a time. Boundary fixtures own real subprocesses, and this local cap
keeps aggregate memory predictable; CI retains the existing two-worker budget.
`npm test`, `npm run docs:check`, and `npm run docs:generate` share
repository-local locks and fail fast when the same class of command is already
running. The `npm test` build is inside that lock. `check:pr` runs its test task
before starting documentation validation, so TypeDoc does not compete with the
full suite for memory.
Vitest and Node persistent compile caches are deliberately not enabled by
default. To evaluate repeated local runs, opt in for both cold and warm
measurements with an isolated cache:

View File

@ -1,20 +1,20 @@
{
"schema_version": 1,
"manifest_id": "ecm_bdccc0a41764d8c182b92a1c9b25e0f08a4b6e8224b35cb83904079a93547d4c",
"manifest_id": "ecm_aba877529714e59795e8a7b51c5513b17081bd701cea1ffee31422a1d95dfa31",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
},
"environment": {
"platform": "linux",
"architecture": "x64",
"platform": "portable",
"architecture": "portable",
"runtime": "node",
"runtime_version": "24"
"runtime_version": "portable"
},
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "94132dd32216783a2e6ed65a5e78e6a0ad645c22ee7ebaf1f1f4ddf6fd110daf"
"sha256": "43fccde70ab600dfc86785a37b414865cb8c0f2cb479eb4ed6c2a90998e0f6a6"
}
],
"claims": [

View File

@ -2,7 +2,7 @@
"$schema": "./node_modules/knip/schema.json",
"entry": ["src/main.ts", "src/cli.ts"],
"project": ["src/**/*.ts", "tests/**/*.ts", "scripts/**/*.mjs"],
"ignoreDependencies": [],
"ignoreDependencies": ["electron"],
"ignoreBinaries": ["ps"],
"ignoreExportsUsedInFile": true,
"ignoreIssues": {

View File

@ -1,5 +1,6 @@
{
"name": "rea-agents",
"mcpName": "io.github.morluto/rea",
"version": "3.0.0",
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
"license": "MIT",
@ -44,6 +45,8 @@
"bridge/ghidra/ReaGhidraBridge.java",
"scripts/rea.mjs",
"scripts/package-runner-bootstrap.mjs",
"scripts/electron-active-hook.cjs",
"scripts/electron-active-hook-boundaries.cjs",
"scripts/hopper-demo-x11.py",
"scripts/prepack.mjs",
"scripts/prepare.mjs",
@ -55,6 +58,7 @@
"deps:check": "node scripts/check-dependency-install.mjs",
"metadata:generate": "node scripts/generate-package-metadata.mjs",
"metadata:check": "node scripts/generate-package-metadata.mjs --check",
"mcp-registry:check": "node scripts/verify-mcp-registry-metadata.mjs",
"mcp-catalog:generate": "npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs",
"mcp-catalog:check": "node scripts/generate-mcp-tool-catalog.mjs --check",
"prebuild": "npm run deps:check && npm run metadata:check",
@ -68,7 +72,7 @@
"precheck:test": "npm run deps:check",
"check:test": "turbo run typecheck lint format:check knip metadata:check test:run",
"precheck:pr": "npm run deps:check",
"check:pr": "turbo run typecheck lint format:check knip metadata:check test:run docs:check:built",
"check:pr": "turbo run typecheck lint format:check knip metadata:check test:run && npm run docs:check",
"precheck:ci": "npm run deps:check",
"check:ci": "turbo run typecheck lint format:check knip metadata:check jscpd scan:todos",
"build:cached": "turbo run build",
@ -81,9 +85,10 @@
"jscpd": "jscpd src/ tests/",
"scan:todos": "node scripts/scan-todos.mjs",
"start": "node scripts/rea.mjs mcp",
"pretest": "npm run build:cached",
"test": "vitest run",
"test:run": "vitest run",
"test": "node scripts/run-exclusive.mjs test npm run test:with-build",
"test:with-build": "npm run build:cached && vitest run",
"test:run": "node scripts/run-exclusive.mjs test vitest run",
"test:local": "node scripts/run-exclusive.mjs test vitest run --changed --passWithNoTests --maxWorkers=1",
"pretest:fast": "npm run build:cached",
"test:fast": "vitest run --project domain --project services --project adapters --project composition --project boundary --project mcp-boundary --project conformance",
"pretest:boundary": "npm run build:cached",
@ -115,6 +120,7 @@
"verify:ghidra": "npm run build:cached && node scripts/verify-real-ghidra.mjs",
"verify:ghidra:windows": "npm run build:cached && npm run build:ghidra-windows-fixture && node scripts/verify-real-ghidra-windows.mjs",
"verify:browser": "npm run build:cached && node scripts/verify-real-browser.mjs",
"verify:electron": "npm run build:cached && node scripts/verify-real-electron.mjs",
"verify:inspector": "npm run build:cached && node scripts/verify-real-javascript-runtime-observation.mjs",
"verify:javascript-runtime": "npm run build:cached && node scripts/verify-local-javascript-runtime.mjs",
"verify:application-workflows": "npm run build:cached && node scripts/verify-local-application-workflows.mjs",
@ -136,8 +142,9 @@
"evidence:check": "npm run build:cached && node scripts/generate-completion-ledger.mjs --check",
"docs:api": "typedoc",
"docs:api:cached": "turbo run docs:api",
"docs:generate": "npm run metadata:generate && npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && node scripts/generate-completion-ledger.mjs && npm run docs:api:cached && node scripts/generate-error-schema.mjs",
"docs:check": "turbo run docs:check:built",
"docs:generate": "node scripts/run-exclusive.mjs docs npm run docs:generate:unlocked",
"docs:generate:unlocked": "npm run metadata:generate && npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && node scripts/generate-completion-ledger.mjs && npm run docs:api:cached && node scripts/generate-error-schema.mjs",
"docs:check": "node scripts/run-exclusive.mjs docs turbo run docs:check:built",
"docs:check:built": "node scripts/generate-package-metadata.mjs --check && node scripts/generate-mcp-tool-catalog.mjs --check && node scripts/generate-skill-metadata.mjs --check && node scripts/generate-product-catalog.mjs --check && node scripts/generate-completion-ledger.mjs --check && node scripts/check-doc-facts.mjs && node scripts/generate-error-schema.mjs --check",
"docs:error-schema": "npm run build:cached && node scripts/generate-error-schema.mjs",
"prepack": "node scripts/prepack.mjs",

View File

@ -25,6 +25,16 @@
"type": "generic",
"path": "src/generatedPackageMetadata.ts"
},
{
"type": "json",
"path": "server.json",
"jsonpath": "$.version"
},
{
"type": "json",
"path": "server.json",
"jsonpath": "$.packages[0].version"
},
{
"type": "json",
"path": "docs/product-catalog.json",

View File

@ -0,0 +1,394 @@
"use strict";
// oxlint-disable-next-line max-lines-per-function -- all adapters must close over one bounded capture context.
const createElectronActiveBoundaryPatches = ({
identity,
patchEmitter,
processType,
recordIpc,
recordRuntime,
shape,
}) => {
const patchedSessions = new WeakSet();
const patchIpcRenderer = (ipcRenderer) => {
if (ipcRenderer === null || ipcRenderer === undefined) return;
for (const [method, kind] of [
["send", "ipc-renderer-send"],
["sendSync", "ipc-renderer-send"],
["invoke", "ipc-renderer-invoke"],
["postMessage", "ipc-renderer-post-message"],
]) {
const original = ipcRenderer[method];
if (typeof original !== "function") continue;
ipcRenderer[method] = function patchedRendererIpc(channel, ...args) {
const recorded = recordIpc(kind, channel, args, {
direction: "renderer-to-main",
sender: processType(),
receiver: "main",
frame: null,
process_type: processType(),
error: false,
});
try {
const result = original.call(this, channel, ...args);
if (result !== null && typeof result?.then === "function")
return result.then(
(value) => {
if (recorded !== null) recorded.result_shape = shape(value);
return value;
},
(cause) => {
if (recorded !== null) recorded.error = true;
throw cause;
},
);
if (recorded !== null) recorded.result_shape = shape(result);
return result;
} catch (cause) {
if (recorded !== null) recorded.error = true;
throw cause;
}
};
}
};
const patchContextBridge = (contextBridge) => {
if (contextBridge === null || contextBridge === undefined) return;
for (const method of ["exposeInMainWorld", "exposeInIsolatedWorld"]) {
const original = contextBridge[method];
if (typeof original !== "function") continue;
contextBridge[method] = function patchedContextBridge(...args) {
const target = typeof args[0] === "string" ? args[0] : null;
recordRuntime("preload", "context-bridge-exposure", "attempted", {
target,
argument_shapes: args.map((value) => shape(value)),
process_type: processType(),
});
try {
const result = original.apply(this, args);
recordRuntime("preload", "context-bridge-exposure", "completed", {
target,
process_type: processType(),
});
return result;
} catch (cause) {
recordRuntime("preload", "context-bridge-exposure", "failed", {
target,
error: true,
process_type: processType(),
});
throw cause;
}
};
}
};
const patchSession = (session) => {
if (session === null || session === undefined) return;
if (
(typeof session !== "object" && typeof session !== "function") ||
patchedSessions.has(session)
)
return;
patchedSessions.add(session);
patchEmitter(session, (event, args) => {
const kind =
event === "will-download"
? "download"
: event.includes("certificate") || event.includes("permission")
? "permission"
: event.includes("window") || event.includes("popup")
? "popup-attempt"
: "web-contents-lifecycle";
if (event === "will-download") {
const cancel = args[1]?.cancel;
if (typeof cancel === "function") cancel.call(args[1]);
}
recordRuntime(
kind,
event,
event === "will-download" ? "blocked" : "observed",
{
target: identity(args[0], "webContents"),
argument_shapes: args.map((value) => shape(value)),
error: event === "will-download",
},
);
});
const originalPermissionRequest = session.setPermissionRequestHandler;
if (typeof originalPermissionRequest === "function")
session.setPermissionRequestHandler =
function blockedPermissionRequest() {
return originalPermissionRequest.call(
this,
(webContents, permission, callback) => {
recordRuntime("permission", "permission-request", "blocked", {
target: identity(webContents, "webContents"),
argument_shapes: [shape(permission)],
error: true,
});
if (typeof callback === "function") callback(false);
},
);
};
const originalPermissionCheck = session.setPermissionCheckHandler;
if (typeof originalPermissionCheck === "function")
session.setPermissionCheckHandler = function blockedPermissionCheck() {
return originalPermissionCheck.call(this, (webContents, permission) => {
recordRuntime("permission", "permission-check", "blocked", {
target: identity(webContents, "webContents"),
argument_shapes: [shape(permission)],
error: true,
});
return false;
});
};
const originalCertificate = session.setCertificateVerifyProc;
if (typeof originalCertificate === "function")
session.setCertificateVerifyProc = function blockedCertificate() {
return originalCertificate.call(this, (request, callback) => {
recordRuntime("permission", "certificate-error", "blocked", {
argument_shapes: [shape(request)],
error: true,
});
if (typeof callback === "function") callback(-2);
});
};
};
const patchWindowOpenHandler = (webContents, contentsId) => {
if (webContents === null || webContents === undefined) return;
const original = webContents.setWindowOpenHandler;
if (typeof original !== "function") return;
const createGuardedHandler =
(handler) =>
(...args) => {
try {
if (typeof handler === "function") handler(...args);
} catch {
recordRuntime("popup-attempt", "window-open-handler", "failed", {
target: contentsId,
argument_shapes: args.map((value) => shape(value)),
error: true,
});
return { action: "deny" };
}
recordRuntime("popup-attempt", "window-open-handler", "blocked", {
target: contentsId,
argument_shapes: args.map((value) => shape(value)),
error: true,
});
return { action: "deny" };
};
try {
original.call(webContents, createGuardedHandler(undefined));
} catch {
recordRuntime("popup-attempt", "window-open-handler-install", "failed", {
target: contentsId,
error: true,
});
}
webContents.setWindowOpenHandler = function blockedWindowOpenHandler(
handler,
) {
return original.call(this, createGuardedHandler(handler));
};
};
const patchSessionManager = (sessionModule) => {
if (sessionModule === null || sessionModule === undefined) return;
patchSession(sessionModule.defaultSession);
for (const method of ["fromPartition", "fromPath"]) {
const original = sessionModule[method];
if (typeof original !== "function") continue;
sessionModule[method] = function patchedSessionFactory(...args) {
const session = original.apply(this, args);
patchSession(session);
return session;
};
}
};
const patchApplicationEffects = (app) => {
if (app === null || app === undefined) return;
for (const method of [
"relaunch",
"setAsDefaultProtocolClient",
"setLoginItemSettings",
]) {
const original = app[method];
if (typeof original !== "function") continue;
app[method] = function blockedApplicationEffect(...args) {
recordRuntime(
method === "relaunch" ? "updater" : "protocol",
method,
"blocked",
{ argument_shapes: args.map((value) => shape(value)), error: true },
);
return method === "setAsDefaultProtocolClient" ? false : undefined;
};
}
};
const patchNativeAddonLoading = () => {
const original = process.dlopen;
if (typeof original !== "function") return;
process.dlopen = function patchedDlopen(module, filename, ...args) {
let digest = null;
try {
const fs = require("node:fs");
const crypto = require("node:crypto");
const stat = fs.statSync(filename);
if (stat.isFile() && stat.size <= 16 * 1024 * 1024)
digest = crypto
.createHash("sha256")
.update(fs.readFileSync(filename))
.digest("hex");
} catch {
digest = null;
}
recordRuntime("native-addon", "dlopen", "attempted", {
artifact_path: typeof filename === "string" ? filename : null,
artifact_sha256: digest,
argument_shapes: [
shape(module),
shape(filename),
...args.map((value) => shape(value)),
],
});
try {
const result = original.call(this, module, filename, ...args);
recordRuntime("native-addon", "dlopen", "completed", {
artifact_path: typeof filename === "string" ? filename : null,
artifact_sha256: digest,
});
return result;
} catch (cause) {
recordRuntime("native-addon", "dlopen", "failed", {
artifact_path: typeof filename === "string" ? filename : null,
artifact_sha256: digest,
error: true,
});
throw cause;
}
};
};
const patchUtilityProcess = (utilityProcess) => {
if (utilityProcess === null || utilityProcess === undefined) return;
const original = utilityProcess.fork;
if (typeof original !== "function") return;
utilityProcess.fork = function patchedFork(...args) {
recordRuntime("process-lifecycle", "utility-process-fork", "attempted", {
argument_shapes: args.map((value) => shape(value)),
process_type: "main",
});
const child = original.apply(this, args);
const childId = identity(child, "utility");
recordRuntime("process-lifecycle", "utility-process-fork", "completed", {
target: childId,
process_type: "main",
});
patchEmitter(child, (event, eventArgs) => {
if (event === "message") {
recordIpc("ipc-utility-to-main", null, eventArgs, {
direction: "utility-to-main",
sender: childId,
receiver: "main",
target: childId,
process_type: "main",
error: false,
});
return;
}
recordRuntime("process-lifecycle", `utility.${event}`, "observed", {
target: childId,
process_type: "utility",
argument_shapes: eventArgs.map((value) => shape(value)),
});
});
if (child && typeof child.postMessage === "function") {
const postMessage = child.postMessage.bind(child);
child.postMessage = (...messageArgs) => {
const recorded = recordIpc(
"utility-process-message",
null,
messageArgs,
{
direction: "main-to-utility",
sender: "main",
receiver: childId,
target: childId,
process_type: "main",
error: false,
},
);
try {
const result = postMessage(...messageArgs);
if (recorded !== null) recorded.result_shape = shape(result);
return result;
} catch (cause) {
if (recorded !== null) recorded.error = true;
throw cause;
}
};
}
return child;
};
};
const patchShell = (shell) => {
if (shell === null || shell === undefined) return;
for (const method of ["openExternal", "openPath"]) {
const original = shell[method];
if (typeof original !== "function") continue;
shell[method] = function blockedShellCall(...args) {
recordRuntime("shell-attempt", method, "blocked", {
argument_shapes: args.map((value) => shape(value)),
error: true,
});
return Promise.reject(new Error("External shell effects are blocked"));
};
}
};
const patchChildProcess = () => {
const childProcess = require("node:child_process");
for (const method of ["spawn", "fork", "exec", "execFile"]) {
const original = childProcess[method];
if (typeof original !== "function") continue;
childProcess[method] = function patchedChildProcess(...args) {
recordRuntime("process-lifecycle", `child.${method}`, "attempted", {
argument_shapes: args.map((value) => shape(value)),
});
const child = original.apply(this, args);
const childId = identity(child, "child");
recordRuntime("process-lifecycle", `child.${method}`, "completed", {
target: childId,
});
patchEmitter(child, (event, eventArgs) =>
recordRuntime("process-lifecycle", `child.${event}`, "observed", {
target: childId,
argument_shapes: eventArgs.map((value) => shape(value)),
}),
);
return child;
};
}
};
return {
patchApplicationEffects,
patchChildProcess,
patchContextBridge,
patchIpcRenderer,
patchNativeAddonLoading,
patchSession,
patchSessionManager,
patchShell,
patchWindowOpenHandler,
patchUtilityProcess,
};
};
module.exports = { createElectronActiveBoundaryPatches };

View File

@ -0,0 +1,501 @@
"use strict";
const {
createElectronActiveBoundaryPatches,
} = require("./electron-active-hook-boundaries.cjs");
const boundedEnvironmentNumber = (name, fallback) => {
const value = Number(process.env[name]);
return Number.isInteger(value)
? Math.min(20_000, Math.max(1, value))
: fallback;
};
const MAX_EVENT_NAME_LENGTH = 128;
const MAX_CHANNEL_LENGTH = 1_024;
const MAX_IDENTIFIER_LENGTH = 256;
const MAX_ARGUMENT_SHAPES = 32;
const maxEvents = boundedEnvironmentNumber(
"REA_ELECTRON_ACTIVE_MAX_RUNTIME_EVENTS",
5_000,
);
const ipcEventKinds = new Set([
"main-handler-invocation",
"main-event-invocation",
"utility-process-fork",
"utility-process-message",
"ipc-main-to-renderer",
"ipc-utility-to-main",
"ipc-renderer-send",
"ipc-renderer-invoke",
"ipc-renderer-post-message",
]);
const events = [];
let observed = 0;
let observedIpc = 0;
let observedRuntime = 0;
let truncated = false;
let sequence = 0;
let correlationSequence = 0;
const boundedString = (value, maximum) =>
typeof value === "string" ? value.slice(0, maximum) : null;
const shape = (value, depth = 0) => {
if (depth > 2 || value === null) return value === null ? "null" : "nested";
if (Array.isArray(value)) return "array";
switch (typeof value) {
case "string":
return "string";
case "number":
return "number";
case "boolean":
return "boolean";
case "undefined":
return "undefined";
case "function":
return "function";
case "object":
return "object";
default:
return "unknown";
}
};
const isAllowedNavigation = (value) => {
if (typeof value !== "string") return true;
try {
const parsed = new URL(value);
if (["file:", "data:", "about:"].includes(parsed.protocol)) return true;
return (
["http:", "https:"].includes(parsed.protocol) &&
["127.0.0.1", "localhost", "[::1]", "::1"].includes(parsed.hostname)
);
} catch {
return false;
}
};
const processType = () =>
typeof process.type === "string" && process.type.length > 0
? process.type.slice(0, MAX_IDENTIFIER_LENGTH)
: "main";
const identity = (value, prefix) => {
if (value === null || value === undefined) return null;
const id = value.id;
const processId = value.pid;
return typeof id === "string" || typeof id === "number"
? `${prefix}:${String(id)}`.slice(0, MAX_IDENTIFIER_LENGTH)
: typeof processId === "number"
? `${prefix}:pid:${String(processId)}`.slice(0, MAX_IDENTIFIER_LENGTH)
: null;
};
const frameIdentity = (event) => {
const frame = event?.senderFrame ?? event?.frame;
const id = frame?.frameTreeNodeId ?? frame?.routingId ?? frame?.id;
return typeof id === "string" || typeof id === "number"
? `frame:${String(id)}`.slice(0, MAX_IDENTIFIER_LENGTH)
: null;
};
const record = (event) => {
observed += 1;
if (ipcEventKinds.has(event.kind)) observedIpc += 1;
else observedRuntime += 1;
if (events.length >= maxEvents) {
truncated = true;
return null;
}
const raw = {
sequence: ++sequence,
correlation_id:
typeof event.correlation_id === "string"
? event.correlation_id
: `capture:${process.pid}:${++correlationSequence}`,
event: null,
phase: "observed",
channel: null,
channel_truncated: false,
direction: null,
sender: null,
receiver: null,
frame: null,
target: null,
argument_shapes: [],
argument_shapes_truncated: false,
result_shape: null,
process_type: processType(),
source: "electron-active-hook",
capture_method: "api-wrapper",
artifact_path: null,
artifact_sha256: null,
error: false,
...event,
};
const argumentShapes = Array.isArray(raw.argument_shapes)
? raw.argument_shapes
: [];
events.push({
...raw,
correlation_id: boundedString(raw.correlation_id, MAX_IDENTIFIER_LENGTH),
event: boundedString(raw.event, MAX_EVENT_NAME_LENGTH),
channel: boundedString(raw.channel, MAX_CHANNEL_LENGTH),
channel_truncated:
raw.channel_truncated === true ||
(typeof raw.channel === "string" &&
raw.channel.length > MAX_CHANNEL_LENGTH),
sender: boundedString(raw.sender, MAX_IDENTIFIER_LENGTH),
receiver: boundedString(raw.receiver, MAX_IDENTIFIER_LENGTH),
frame: boundedString(raw.frame, MAX_IDENTIFIER_LENGTH),
target: boundedString(raw.target, MAX_IDENTIFIER_LENGTH),
argument_shapes: argumentShapes
.slice(0, MAX_ARGUMENT_SHAPES)
.map((value) => boundedString(value, 64) ?? "unknown"),
argument_shapes_truncated:
raw.argument_shapes_truncated === true ||
argumentShapes.length > MAX_ARGUMENT_SHAPES,
result_shape: boundedString(raw.result_shape, 64),
process_type: boundedString(raw.process_type, MAX_IDENTIFIER_LENGTH),
artifact_path: boundedString(raw.artifact_path, 16_384),
artifact_sha256: boundedString(raw.artifact_sha256, 64),
});
return events.at(-1);
};
const recordRuntime = (kind, event, phase, details = {}) =>
record({ kind, event, phase, ...details });
const recordIpc = (kind, channel, args, details = {}) =>
(() => {
const values = Array.isArray(args) ? args : [];
return record({
kind,
channel: typeof channel === "string" ? channel : null,
argument_shapes: values
.slice(0, MAX_ARGUMENT_SHAPES)
.map((value) => shape(value)),
argument_shapes_truncated: values.length > MAX_ARGUMENT_SHAPES,
...details,
});
})();
const patchEmitter = (emitter, observeEvent) => {
if (emitter === null || emitter === undefined) return;
const original = emitter.emit;
if (typeof original !== "function") return;
if (emitter.__reaElectronActiveEmitPatched === true) return;
try {
emitter.__reaElectronActiveEmitPatched = true;
emitter.emit = function patchedEmit(event, ...args) {
if (typeof event === "string") observeEvent.call(this, event, args);
return original.call(this, event, ...args);
};
} catch {
globalThis.__reaElectronActiveHookError = true;
}
};
const recordInvocation = (kind, channel, event, args) =>
recordIpc(kind, channel, args, {
direction: "renderer-to-main",
sender: identity(event?.sender, "webContents"),
receiver: "main",
frame: frameIdentity(event),
process_type: "main",
error: false,
});
const patchIpcMain = (ipcMain) => {
if (ipcMain === null || ipcMain === undefined) return;
for (const [method, kind] of [
["handle", "main-handler-invocation"],
["on", "main-event-invocation"],
["once", "main-event-invocation"],
]) {
const original = ipcMain[method];
if (typeof original !== "function") continue;
ipcMain[method] = function patchedIpcMain(channel, listener) {
if (typeof listener !== "function")
return original.call(this, channel, listener);
const wrapped =
kind === "main-handler-invocation"
? async function wrappedHandler(event, ...args) {
const recorded = recordInvocation(kind, channel, event, args);
try {
const result = await listener.call(this, event, ...args);
if (recorded !== null) recorded.result_shape = shape(result);
return result;
} catch (cause) {
if (recorded !== null) recorded.error = true;
throw cause;
}
}
: function wrappedEventListener(event, ...args) {
const recorded = recordInvocation(kind, channel, event, args);
try {
return listener.call(this, event, ...args);
} catch (cause) {
if (recorded !== null) recorded.error = true;
throw cause;
}
};
return original.call(this, channel, wrapped);
};
}
};
const patchNavigationMethods = (webContents, contentsId) => {
for (const method of ["loadURL", "loadFile", "loadDataURL"]) {
const original = webContents[method];
if (typeof original !== "function") continue;
webContents[method] = function patchedNavigation(...args) {
recordRuntime("navigation", method, "attempted", {
target: contentsId,
argument_shapes: args.map((value) => shape(value)),
});
if (method === "loadURL" && !isAllowedNavigation(args[0])) {
recordRuntime("navigation", method, "blocked", {
target: contentsId,
error: true,
});
return Promise.reject(new Error("External navigation is blocked"));
}
try {
const result = original.apply(this, args);
if (result !== null && typeof result?.then === "function")
return result.then(
(value) => {
recordRuntime("navigation", method, "completed", {
target: contentsId,
});
return value;
},
(cause) => {
recordRuntime("navigation", method, "failed", {
target: contentsId,
error: true,
});
throw cause;
},
);
recordRuntime("navigation", method, "completed", {
target: contentsId,
});
return result;
} catch (cause) {
recordRuntime("navigation", method, "failed", {
target: contentsId,
error: true,
});
throw cause;
}
};
}
};
const patchWebContentsIpc = (webContents, contentsId) => {
const originalSend = webContents.send;
if (typeof originalSend === "function")
webContents.send = function patchedSend(channel, ...args) {
const recorded = recordIpc("ipc-main-to-renderer", channel, args, {
direction: "main-to-renderer",
sender: "main",
receiver: contentsId,
target: contentsId,
process_type: "main",
error: false,
});
try {
const result = originalSend.call(this, channel, ...args);
if (recorded !== null) recorded.result_shape = shape(result);
return result;
} catch (cause) {
if (recorded !== null) recorded.error = true;
throw cause;
}
};
const originalPostMessage = webContents.postMessage;
if (typeof originalPostMessage === "function")
webContents.postMessage = function patchedPostMessage(
channel,
message,
transfer,
) {
const recorded = recordIpc(
"ipc-main-to-renderer",
channel,
[message, transfer],
{
direction: "main-to-renderer",
sender: "main",
receiver: contentsId,
target: contentsId,
process_type: "main",
error: false,
},
);
try {
const result = originalPostMessage.call(
this,
channel,
message,
transfer,
);
if (recorded !== null) recorded.result_shape = shape(result);
return result;
} catch (cause) {
if (recorded !== null) recorded.error = true;
throw cause;
}
};
};
const patchWebContents = (webContents, windowId) => {
if (webContents === null || webContents === undefined) return;
const contentsId = identity(webContents, "webContents");
patchEmitter(webContents, (event, args) => {
const navigation = [
"did-start-loading",
"did-stop-loading",
"did-finish-load",
"did-fail-load",
"did-frame-finish-load",
"will-navigate",
"will-redirect",
].includes(event);
const kind = navigation
? "navigation"
: [
"console-message",
"render-process-gone",
"crashed",
"unresponsive",
"responsive",
].includes(event)
? "error"
: event === "did-create-window"
? "popup-attempt"
: "web-contents-lifecycle";
if (event === "will-navigate" || event === "will-redirect") {
if (!isAllowedNavigation(args[1])) {
const preventDefault = args[0]?.preventDefault;
if (typeof preventDefault === "function") preventDefault.call(args[0]);
recordRuntime("navigation", event, "blocked", {
target: contentsId,
sender: windowId,
argument_shapes: args.map((value) => shape(value)),
error: true,
});
return;
}
}
recordRuntime(kind, event, "observed", {
target: contentsId,
sender: windowId,
argument_shapes: args.map((value) => shape(value)),
error: kind === "error",
});
if (event === "did-create-window") {
const childWindow = args[0];
patchWebContents(
childWindow?.webContents,
identity(childWindow, "window"),
);
}
});
patchNavigationMethods(webContents, contentsId);
patchWebContentsIpc(webContents, contentsId);
boundaries.patchWindowOpenHandler(webContents, contentsId);
};
const patchBrowserWindow = (electron) => {
const original = electron.BrowserWindow;
if (typeof original !== "function") return;
electron.BrowserWindow = new Proxy(original, {
construct(target, args, newTarget) {
const window = Reflect.construct(target, args, newTarget);
const targetId = identity(window, "window");
recordRuntime("window-lifecycle", "created", "completed", {
target: targetId,
argument_shapes: args.map((value) => shape(value)),
});
const preload = args[0]?.webPreferences?.preload;
if (typeof preload === "string")
recordRuntime("preload", "configured", "completed", {
target: targetId,
artifact_path: preload,
});
patchEmitter(window, (event) =>
recordRuntime("window-lifecycle", event, "observed", {
target: targetId,
}),
);
patchWebContents(window.webContents, targetId);
return window;
},
});
};
const boundaries = createElectronActiveBoundaryPatches({
identity,
patchEmitter,
processType,
recordIpc,
recordRuntime,
shape,
});
try {
const electron = require("electron");
patchEmitter(electron.app, (event, args) =>
recordRuntime(
event === "open-url" || event === "second-instance"
? "protocol"
: "app-lifecycle",
event,
"observed",
{
target: "app",
argument_shapes: args.map((value) => shape(value)),
process_type: "main",
},
),
);
patchIpcMain(electron.ipcMain);
boundaries.patchIpcRenderer(electron.ipcRenderer);
boundaries.patchContextBridge(electron.contextBridge);
patchBrowserWindow(electron);
boundaries.patchUtilityProcess(electron.utilityProcess);
boundaries.patchShell(electron.shell);
boundaries.patchSessionManager(electron.session);
boundaries.patchApplicationEffects(electron.app);
boundaries.patchNativeAddonLoading();
boundaries.patchChildProcess();
process.once("uncaughtException", (cause, origin) => {
recordRuntime("process-lifecycle", "uncaught-exception", "observed", {
argument_shapes: [shape(cause), shape(origin)],
error: true,
});
throw cause;
});
process.once("unhandledRejection", (reason, promise) => {
recordRuntime("process-lifecycle", "unhandled-rejection", "observed", {
argument_shapes: [shape(reason), shape(promise)],
error: true,
});
throw reason;
});
} catch {
globalThis.__reaElectronActiveHookError = true;
}
globalThis.__reaElectronActiveSnapshot = () => ({
events: events.slice(),
observed,
observed_ipc: observedIpc,
observed_runtime: observedRuntime,
truncated,
hook_error: globalThis.__reaElectronActiveHookError === true,
});

View File

@ -30,16 +30,27 @@ const { stdout } = await exec(
},
);
const verifierOutput = JSON.parse(stdout);
// Managed conformance claims are host-independent. Keep the checked-in
// commitment reproducible across macOS, Linux, and Windows rather than
// embedding the machine that happened to generate it.
const portableCompletionReport = {
...verifierOutput.completionReport,
environment: {
platform: "portable",
architecture: "portable",
runtime: "node",
runtime_version: "portable",
},
};
const { createCompletionLedgerArtifacts } = await import(
`../dist/domain/completionLedgerGeneration.js?${String(Date.now())}`
);
const skillDigest = await directoryDigest(
join(root, "skills/reverse-engineer-anything"),
);
const generated = createCompletionLedgerArtifacts(
verifierOutput.completionReport,
[{ skill_id: "reverse-engineer-anything", sha256: skillDigest }],
);
const generated = createCompletionLedgerArtifacts(portableCompletionReport, [
{ skill_id: "reverse-engineer-anything", sha256: skillDigest },
]);
if (!check) await mkdir(outputRoot, { recursive: true });
await Promise.all([

View File

@ -17,6 +17,9 @@ for (const argument of arguments_)
throw new Error(`Unknown MCP tool catalog option: ${argument}`);
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
// The checked-in catalog is a portable package artifact. Keep its canonical
// provider metadata independent of the machine running the generator.
const CATALOG_PLATFORM = "darwin";
const sessionToolNames = new Set([
...TOOL_CONTRACTS.filter(({ kind }) => kind === "session").map(
({ name }) => name,
@ -55,13 +58,26 @@ const catalog = TOOL_CONTRACTS.map((contract) => {
});
const auxiliaryProviders = [
new ArtifactProvider(false, false),
new NativeMacOSProvider(),
new NativeMacOSProvider(undefined, CATALOG_PLATFORM),
new ManagedStaticProvider(),
].map((provider) => ({
identity: provider.identity(),
capabilities: provider.capabilities(),
}));
const payloadJson = JSON.stringify({ catalog, auxiliaryProviders });
const canonicalizeJson = (value) => {
if (Array.isArray(value)) return value.map(canonicalizeJson);
if (value === null || typeof value !== "object") return value;
return Object.fromEntries(
Object.entries(value)
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
.map(([key, entry]) => [key, canonicalizeJson(entry)]),
);
};
const payloadJson = JSON.stringify(
canonicalizeJson({ catalog, auxiliaryProviders }),
);
const source = await format(
`import type { ToolAnnotations } from "@modelcontextprotocol/server";
import type {

View File

@ -81,26 +81,12 @@ export const toolFamilyCatalog = (sources) => {
/** Build provider identities with their sorted capability names. */
export const providerCatalog = (sources) => {
const applicationContracts =
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
({ name }) => name === "analyze_javascript_application",
);
const reconciliationContracts =
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
({ name }) => name === "reconcile_javascript_runtime",
);
const observationContracts =
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
({ name }) =>
name !== "analyze_javascript_application" &&
name !== "reconcile_javascript_runtime",
);
if (
applicationContracts.length !== 1 ||
reconciliationContracts.length !== 1 ||
observationContracts.length !== 2
)
throw new Error("Electron provider capability ownership drifted");
const {
applicationContracts,
reconciliationContracts,
observationContracts,
activeContracts,
} = electronContractSlices(sources);
return [
{
identity: sources.hopperProvider.HOPPER_PROVIDER_IDENTITY,
@ -142,6 +128,12 @@ export const providerCatalog = (sources) => {
identity: sources.electronProvider.CDP_ELECTRON_PROVIDER_IDENTITY,
contracts: observationContracts,
},
{
identity:
sources.electronActiveProvider
.PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY,
contracts: activeContracts,
},
{
identity: sources.v8InspectorProvider.V8_INSPECTOR_PROVIDER_IDENTITY,
contracts:
@ -171,3 +163,38 @@ export const providerCatalog = (sources) => {
}))
.sort((left, right) => left.id.localeCompare(right.id));
};
const electronContractSlices = (sources) => {
const applicationContracts =
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
({ name }) => name === "analyze_javascript_application",
);
const reconciliationContracts =
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
({ name }) => name === "reconcile_javascript_runtime",
);
const observationContracts =
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
({ name }) =>
name !== "analyze_javascript_application" &&
name !== "reconcile_javascript_runtime" &&
name !== "capture_electron_scenario",
);
const activeContracts =
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
({ name }) => name === "capture_electron_scenario",
);
if (
applicationContracts.length !== 1 ||
reconciliationContracts.length !== 1 ||
observationContracts.length !== 2 ||
activeContracts.length !== 1
)
throw new Error("Electron provider capability ownership drifted");
return {
applicationContracts,
reconciliationContracts,
observationContracts,
activeContracts,
};
};

View File

@ -55,6 +55,7 @@ const SOURCE_PATHS = {
browserProvider: "dist/browser/CdpBrowserProvider.js",
browserScenarioProvider: "dist/browser/PlaywrightBrowserScenarioProvider.js",
electronProvider: "dist/browser/CdpElectronProvider.js",
electronActiveProvider: "dist/browser/PlaywrightElectronActiveProvider.js",
v8InspectorProvider: "dist/browser/V8InspectorProvider.js",
evidence: "dist/domain/evidence.js",
evidenceBundle: "dist/domain/evidenceBundle.js",
@ -70,6 +71,7 @@ const SOURCE_PATHS = {
browserScenarioDiff: "dist/domain/browserScenarioDiff.js",
browserSession: "dist/domain/browserSession.js",
electronObservation: "dist/domain/electronObservation.js",
electronActiveObservation: "dist/domain/electronActiveObservation.js",
javascriptRuntimeObservation: "dist/domain/javascriptRuntimeObservation.js",
webBundleAnalysis: "dist/domain/webBundleAnalysis.js",
webCaptureDiff: "dist/domain/webCaptureDiff.js",

147
scripts/run-exclusive.mjs Normal file
View File

@ -0,0 +1,147 @@
#!/usr/bin/env node
import { spawn } from "node:child_process";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
const [, , lockName, ...command] = process.argv;
const lockRoot = resolve(process.cwd(), ".cache", "rea-command-locks");
const lockPath = join(lockRoot, `${lockName ?? "invalid"}.lock`);
const ownerPath = join(lockPath, "owner.json");
const signalExitCodes = { SIGINT: 130, SIGTERM: 143 };
let lockHeld = false;
let child;
let shutdownSignal;
process.on("SIGINT", () => handleSignal("SIGINT"));
process.on("SIGTERM", () => handleSignal("SIGTERM"));
if (
typeof lockName !== "string" ||
!/^[a-z][a-z0-9-]*$/u.test(lockName) ||
command.length === 0
) {
console.error("Usage: run-exclusive.mjs <lock-name> <command> [args...]");
process.exitCode = 64;
} else {
await run();
}
async function run() {
try {
await acquireLock();
const result = await runCommand();
process.exitCode =
shutdownSignal === undefined
? (result.code ?? 1)
: signalExitCodes[shutdownSignal];
} catch (cause) {
console.error(cause instanceof Error ? cause.message : String(cause));
process.exitCode = 1;
} finally {
await releaseLock();
}
}
async function acquireLock() {
await mkdir(lockRoot, { recursive: true });
for (let attempt = 0; attempt < 3; attempt += 1) {
try {
await mkdir(lockPath);
await writeFile(
ownerPath,
JSON.stringify({
pid: process.pid,
command: command.join(" "),
started_at: new Date().toISOString(),
}),
"utf8",
);
lockHeld = true;
return;
} catch (cause) {
if (!isAlreadyExistsError(cause)) throw cause;
const owner = await readOwner();
if (owner === null || isProcessAlive(owner.pid))
throw new Error(
`Another ${lockName} command is already running (pid ${String(owner?.pid ?? "unknown")}). Wait for it to finish or inspect ${lockPath}.`,
);
await rm(lockPath, { force: true, recursive: true });
}
}
throw new Error(`Could not acquire the ${lockName} command lock`);
}
async function releaseLock() {
if (!lockHeld) return;
lockHeld = false;
try {
const owner = await readOwner();
if (owner?.pid === process.pid)
await rm(lockPath, { force: true, recursive: true });
} catch (cause) {
console.error(
`Could not remove the ${lockName} command lock: ${cause instanceof Error ? cause.message : String(cause)}`,
);
}
}
function runCommand() {
return new Promise((resolveResult, reject) => {
child = spawn(command[0], command.slice(1), {
cwd: process.cwd(),
env: process.env,
shell: process.platform === "win32",
stdio: "inherit",
});
child.once("error", reject);
child.once("close", (code) => resolveResult({ code }));
});
}
function handleSignal(signal) {
if (shutdownSignal !== undefined) return;
shutdownSignal = signal;
if (child !== undefined && child.exitCode === null) child.kill(signal);
}
async function readOwner() {
try {
const parsed = JSON.parse(await readFile(ownerPath, "utf8"));
const pid =
typeof parsed === "object" && parsed !== null
? Reflect.get(parsed, "pid")
: undefined;
if (typeof pid !== "number" || !Number.isInteger(pid)) return null;
return { pid };
} catch (cause) {
if (isMissingFileError(cause)) return null;
throw cause;
}
}
function isProcessAlive(pid) {
try {
process.kill(pid, 0);
return true;
} catch (cause) {
return !isMissingProcessError(cause);
}
}
function isAlreadyExistsError(cause) {
return isNodeError(cause) && cause.code === "EEXIST";
}
function isMissingFileError(cause) {
return isNodeError(cause) && cause.code === "ENOENT";
}
function isMissingProcessError(cause) {
return isNodeError(cause) && cause.code === "ESRCH";
}
function isNodeError(cause) {
return typeof cause === "object" && cause !== null && "code" in cause;
}

View File

@ -12,9 +12,9 @@ import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
const exec = promisify(execFile);
const verifierRun = createVerifierRun();
const manifestPath = await realpath(
resolve(process.argv[2] ?? "build/conformance/manifest.json"),
);
const manifestArgument = process.argv[2] ?? "build/conformance/manifest.json";
const manifestPath = await realpath(resolve(manifestArgument));
const sourceRoot = await resolveSourceRoot(manifestPath, process.argv[3]);
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
if (
manifest.schemaVersion !== 1 ||
@ -39,9 +39,30 @@ if (v1 === undefined || v2 === undefined)
if (v1.artifactSha256 === v2.artifactSha256)
throw new Error("Version-pair artifacts unexpectedly have equal hashes");
process.stdout.write(
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), verified: manifest.fixtures.length, manifestPath, platform: manifest.platform })}\n`,
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), verified: manifest.fixtures.length, manifestPath, sourceRoot, platform: manifest.platform })}\n`,
);
async function resolveSourceRoot(manifestPath, declaredSourceRoot) {
const candidates = [
declaredSourceRoot === undefined
? null
: resolve(process.cwd(), declaredSourceRoot),
resolve(process.cwd(), "tests/conformance"),
resolve(dirname(dirname(dirname(manifestPath))), "tests/conformance"),
resolve(dirname(manifestPath), "../../tests/conformance"),
].filter((candidate) => candidate !== null);
for (const candidate of candidates) {
try {
return await realpath(candidate);
} catch {
// Try the next clean-checkout candidate.
}
}
throw new Error(
"Could not locate tests/conformance; pass its path as the second argument",
);
}
async function verifyFixture(fixture) {
const artifactPath = await confinedPath(root, fixture.artifact);
const sources = [];
@ -51,12 +72,7 @@ async function verifyFixture(fixture) {
const content =
source.path === "generated/large.c"
? generateLargeFixture()
: await readFile(
await confinedPath(
resolve(process.cwd(), "tests/conformance"),
source.path,
),
);
: await readFile(await confinedPath(sourceRoot, source.path));
if (sha256(content) !== source.sha256)
throw new Error(
`${fixture.name}: source hash mismatch for ${source.path}`,

View File

@ -0,0 +1,51 @@
import { readFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const readJson = async (path) =>
JSON.parse(await readFile(join(root, path), "utf8"));
const fail = (message) => {
throw new Error(`MCP Registry metadata is invalid: ${message}`);
};
const packageJson = await readJson("package.json");
const serverJson = await readJson("server.json");
const registryPackage = serverJson.packages?.[0];
if (packageJson.name !== "rea-agents") fail("unexpected npm package name");
if (typeof packageJson.version !== "string")
fail("package.json is missing a string version");
if (packageJson.mcpName !== "io.github.morluto/rea")
fail("package.json mcpName does not identify io.github.morluto/rea");
if (serverJson.name !== packageJson.mcpName)
fail("server.json name does not match package.json mcpName");
if (serverJson.version !== packageJson.version)
fail("server.json version does not match package.json version");
if (!Array.isArray(serverJson.packages) || serverJson.packages.length !== 1)
fail("server.json must describe exactly one npm package");
if (registryPackage === undefined)
fail("server.json is missing its npm package");
if (registryPackage.registryType !== "npm")
fail("server.json package must use the npm registry");
if (registryPackage.registryBaseUrl !== "https://registry.npmjs.org")
fail("server.json package must use the public npm registry");
if (registryPackage.identifier !== packageJson.name)
fail("server.json package identifier does not match package.json name");
if (registryPackage.version !== packageJson.version)
fail("server.json package version does not match package.json version");
if (registryPackage.runtimeHint !== "npx")
fail("server.json package must declare the npx runtime");
if (registryPackage.transport?.type !== "stdio")
fail("server.json package must declare stdio transport");
if (
!Array.isArray(registryPackage.packageArguments) ||
registryPackage.packageArguments.length !== 1 ||
registryPackage.packageArguments[0]?.type !== "positional" ||
registryPackage.packageArguments[0]?.value !== "mcp"
)
fail("server.json package must launch the existing mcp command");
process.stdout.write(
`MCP Registry metadata is aligned for ${packageJson.name}@${packageJson.version}.\n`,
);

View File

@ -26,6 +26,8 @@ export async function verifyPackagePack({ root, workspace }) {
packedManifest.bin?.["rea-agents"] !== "scripts/rea.mjs"
)
throw new Error("package did not expose both rea command entry points");
if (packedManifest.mcpName !== "io.github.morluto/rea")
throw new Error("package did not retain its MCP Registry ownership marker");
if (
packedFiles.some(
(path) => path.includes("__pycache__") || path.endsWith(".pyc"),

View File

@ -0,0 +1,113 @@
#!/usr/bin/env node
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { PlaywrightElectronActiveProvider } from "../dist/browser/PlaywrightElectronActiveProvider.js";
import { electronActiveObservationInputSchema } from "../dist/domain/electronActiveObservation.js";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
const executable = process.env.REA_ELECTRON_EXECUTABLE;
if (executable === undefined || executable.length === 0)
throw new Error(
"REA_ELECTRON_EXECUTABLE must be an absolute path to an approved Electron executable",
);
const repositoryRoot = fileURLToPath(new URL("..", import.meta.url));
const applicationRoot =
process.env.REA_ELECTRON_APPLICATION_ROOT ??
join(repositoryRoot, "tests/conformance/readiness/electron");
const applicationPath =
process.env.REA_ELECTRON_APPLICATION_PATH ?? join(applicationRoot, "main.js");
const input = electronActiveObservationInputSchema.parse({
schema_version: 1,
executable_path: executable,
application_path: applicationPath,
application_root: applicationRoot,
actions: [
{ step_id: "ipc", kind: "click", selector: "#run", window_index: 0 },
{
step_id: "open-url",
kind: "deep-link",
delivery: "open-url",
url: "rea-readiness://open-url",
},
{
step_id: "second-instance",
kind: "deep-link",
delivery: "second-instance",
url: "rea-readiness://second-instance",
},
{ step_id: "renderer-reload", kind: "renderer-reload", window_index: 1 },
{ step_id: "renderer-crash", kind: "renderer-crash", window_index: 1 },
{
step_id: "renderer-restart",
kind: "wait",
duration_ms: 250,
window_index: 1,
},
],
approved: true,
});
const verifierRun = createVerifierRun();
const result = await new PlaywrightElectronActiveProvider().capture(input);
if (!result.ok) throw result.error;
const output = {
verifier_run: await completeVerifierRun(verifierRun),
electron: result.value.application,
actions: result.value.actions,
processes: result.value.processes,
ipc: result.value.ipc,
timeline: result.value.timeline,
windows: result.value.windows,
verified:
result.value.actions.every(({ status }) => status === "completed") &&
result.value.actions.some(
({ kind, window_index }) =>
kind === "renderer-reload" && window_index === 1,
) &&
result.value.actions.some(
({ kind, window_index }) =>
kind === "renderer-crash" && window_index === 1,
) &&
result.value.windows.length >= 2 &&
result.value.ipc.events.some(
({ kind, channel }) =>
kind === "main-handler-invocation" && channel === "readiness:echo",
) &&
result.value.timeline.events.some(
({ kind, event, phase }) =>
kind === "window-lifecycle" &&
event === "created" &&
phase === "completed",
) &&
result.value.timeline.events.some(
({ kind, event }) =>
kind === "process-lifecycle" && event === "utility-process-fork",
) &&
result.value.timeline.events.some(
({ kind, event, phase }) =>
kind === "shell-attempt" &&
event === "openExternal" &&
phase === "blocked",
) &&
result.value.timeline.events.some(
({ kind, event, phase }) =>
kind === "preload" && event === "configured" && phase === "completed",
) &&
result.value.timeline.events.some(
({ kind, event }) =>
kind === "protocol" &&
(event === "open-url" || event === "second-instance"),
) &&
result.value.timeline.events.some(
({ kind, event }) =>
kind === "process-lifecycle" && event === "child.spawn",
),
};
if (!output.verified)
throw new Error(
"real Electron verifier did not observe the readiness IPC round trip",
);
process.stdout.write(`${JSON.stringify(output)}\n`);

30
server.json Normal file
View File

@ -0,0 +1,30 @@
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.morluto/rea",
"title": "REA",
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
"websiteUrl": "https://github.com/morluto/rea",
"repository": {
"url": "https://github.com/morluto/rea",
"source": "github"
},
"version": "3.0.0",
"packages": [
{
"registryType": "npm",
"registryBaseUrl": "https://registry.npmjs.org",
"identifier": "rea-agents",
"version": "3.0.0",
"runtimeHint": "npx",
"transport": {
"type": "stdio"
},
"packageArguments": [
{
"type": "positional",
"value": "mcp"
}
]
}
]
}

View File

@ -3,8 +3,8 @@ name: reverse-engineer-anything
description: Reverse engineer native, managed, Electron/JavaScript, packaged, and browser applications with REA. Use shipped-artifact or approved runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis.
metadata:
version: "23"
tool_count: 115
catalog_digest: "6b2470559f830c70f2b1a1453fd8cf1264f690b6f6718342deb04406aefa9fe1"
tool_count: 116
catalog_digest: "2da444fd95a40c28a6c0332a5970edca6b984f0c2922e4c02eaf7458a6f3dd0e"
---
# REA

View File

@ -41,6 +41,7 @@ export type AvailabilityPolicy = {
readonly browserObservationEnabled?: boolean;
readonly browserScenarioEnabled?: boolean;
readonly electronObservationEnabled?: boolean;
readonly electronAutomationEnabled?: boolean;
readonly v8InspectorObservationEnabled?: boolean;
readonly javascriptReplayEnabled?: boolean;
readonly managedRuntimeEnabled?: boolean;
@ -283,14 +284,8 @@ const policyAvailability = ({
reason: "policy_disabled",
remediation: "Configure or grant an evidence_read root.",
};
if (kind === "electron-provider")
return policy.electronObservationEnabled === true
? { reason: "available", remediation: null }
: {
reason: "policy_disabled",
remediation:
"Enable Electron observation and configure a loopback CDP endpoint and canonical file roots.",
};
const electron = electronPolicyAvailability(name, kind, policy);
if (electron !== null) return electron;
if (kind === "runtime-provider")
return policy.v8InspectorObservationEnabled === true
? { reason: "available", remediation: null }
@ -303,6 +298,29 @@ const policyAvailability = ({
return null;
};
const electronPolicyAvailability = (
name: string,
kind: ToolKind,
policy: AvailabilityPolicy,
): Availability | null => {
if (kind !== "electron-provider") return null;
if (name === "capture_electron_scenario")
return policy.electronAutomationEnabled === true
? { reason: "available", remediation: null }
: {
reason: "policy_disabled",
remediation:
"Enable active Electron automation and configure exact executable and application roots.",
};
return policy.electronObservationEnabled === true
? { reason: "available", remediation: null }
: {
reason: "policy_disabled",
remediation:
"Enable Electron observation and configure a loopback CDP endpoint and canonical file roots.",
};
};
const browserPolicyAvailability = (
name: string,
kind: ToolKind,

View File

@ -0,0 +1,81 @@
import type { ProviderIdentity } from "./AnalysisProvider.js";
import type { Evidence, EvidenceObservation } from "../domain/evidence.js";
import { createEvidence } from "../domain/evidence.js";
import type {
ElectronActiveObservationInput,
ElectronActiveObservationResult,
} from "../domain/electronActiveObservation.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { digestJson } from "./JavaScriptReplayPlanning.js";
/** Create Evidence v2 without retaining arbitrary runtime argument values. */
export const createElectronActiveEvidence = (
input: ElectronActiveObservationInput,
result: ElectronActiveObservationResult,
provider: ProviderIdentity,
): Evidence =>
createEvidence(undefined, provider, {
predicateType: "rea.electron-active-scenario/v1",
operation: "capture_electron_scenario",
parameters: parameters(input),
result: jsonValueSchema.parse(result),
confidence: "observed",
authority: "controlled-replay",
environment: {
id: `${result.application.electron_version}@${process.platform}`,
platform: process.platform,
architecture: process.arch,
isolation: "none",
},
limitations: result.limitations,
});
const scenarioProjection = (
input: ElectronActiveObservationInput,
): EvidenceObservation["parameters"] => ({
approved: input.approved,
schema_version: input.schema_version,
executable_path: input.executable_path,
application_path: input.application_path,
application_root: input.application_root,
args: redactArguments(input.args),
actions: input.actions.map(({ step_id, kind }) => ({ step_id, kind })),
limits: input.limits,
});
const parameters = (
input: ElectronActiveObservationInput,
): EvidenceObservation["parameters"] => ({
...scenarioProjection(input),
scenario_sha256: digestJson(scenarioProjection(input)),
});
const redactArguments = (arguments_: readonly string[]): string[] => {
let redactNext = false;
return arguments_.map((argument) => {
if (redactNext) {
redactNext = false;
return "<redacted>";
}
if (sensitiveFlag.test(argument)) {
redactNext = true;
return argument;
}
return redactArgument(argument);
});
};
const sensitiveFlag =
/^--?(?:password|token|secret|api[-_]?key|authorization|cookie)$/iu;
const redactArgument = (argument: string): string => {
if (
/^--?(?:password|token|secret|api[-_]?key|authorization|cookie)=/iu.test(
argument,
)
)
return `${argument.slice(0, argument.indexOf("=") + 1)}<redacted>`;
return argument.length > 256
? `${argument.slice(0, 256)}<redacted>`
: argument;
};

View File

@ -0,0 +1,16 @@
import type { ExecutionOptions, ProviderIdentity } from "./AnalysisProvider.js";
import type { AnalysisError } from "../domain/errors.js";
import type {
ElectronActiveObservationInput,
ElectronActiveObservationResult,
} from "../domain/electronActiveObservation.js";
import type { Result } from "../domain/result.js";
/** Provider-neutral boundary for owned Electron runtime experiments. */
export interface ElectronActiveObservationPort {
identity(): ProviderIdentity;
capture(
input: ElectronActiveObservationInput,
options?: ExecutionOptions,
): Promise<Result<ElectronActiveObservationResult, AnalysisError>>;
}

View File

@ -0,0 +1,132 @@
import { realpath } from "node:fs/promises";
import type { ExecutionOptions } from "./AnalysisProvider.js";
import type { ElectronActiveObservationPort } from "./ElectronActiveObservationPort.js";
import type { PermissionAuthority } from "./PermissionAuthority.js";
import { digestJson } from "./JavaScriptReplayPlanning.js";
import {
AnalysisCapabilityUnavailableError,
AnalysisInputError,
AnalysisProtocolError,
PermissionRequiredError,
type AnalysisError,
} from "../domain/errors.js";
import type { Evidence } from "../domain/evidence.js";
import type { ElectronActiveObservationInput } from "../domain/electronActiveObservation.js";
import { isPathContained } from "../domain/permissionPolicy.js";
import { err, type Result } from "../domain/result.js";
import { createElectronActiveEvidence } from "./ElectronActiveEvidence.js";
const OPERATION = "capture_electron_scenario" as const;
const PROVIDER_ID = "rea-playwright-electron-active";
const canonicalizeInput = async (
input: ElectronActiveObservationInput,
): Promise<Result<ElectronActiveObservationInput, AnalysisError>> => {
try {
const [executablePath, applicationPath, applicationRoot] =
await Promise.all([
realpath(input.executable_path),
realpath(input.application_path),
realpath(input.application_root),
]);
if (!isPathContained(applicationRoot, applicationPath))
return err(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["application_path"],
reason: "invalid_value",
message:
"application_path must remain beneath application_root after symlink resolution",
},
]),
);
return {
ok: true,
value: {
...input,
executable_path: executablePath,
application_path: applicationPath,
application_root: applicationRoot,
},
};
} catch (cause: unknown) {
return err(
new AnalysisInputError(OPERATION, { cause }, [
{
path: ["application_path"],
reason: "invalid_format",
message:
"Electron executable, application, and root must resolve to existing paths",
},
]),
);
}
};
const authorize = async (
authority: PermissionAuthority | undefined,
input: ElectronActiveObservationInput,
): Promise<Result<ElectronActiveObservationInput, AnalysisError>> => {
const canonical = await canonicalizeInput(input);
if (!canonical.ok) return canonical;
if (authority === undefined)
return err(
new AnalysisCapabilityUnavailableError(
PROVIDER_ID,
OPERATION,
"active Electron observation permission policy is not configured",
),
);
const authorized = await authority.authorize(
{
capability: "electron_automate",
roots: [canonical.value.application_root],
executables: [canonical.value.executable_path],
environment_names: [],
network: "external",
mount: false,
operation_identity: `${OPERATION}:${digestJson(canonical.value)}`,
},
"write",
);
if (!authorized.ok)
return err(
authorized.error instanceof PermissionRequiredError
? authorized.error
: new AnalysisProtocolError(authorized.error.message, {
cause: authorized.error,
}),
);
return canonical;
};
/** Authorize and execute one bounded, provider-owned Electron experiment. */
export const captureElectronScenario = async (
provider: ElectronActiveObservationPort | undefined,
authority: PermissionAuthority | undefined,
input: ElectronActiveObservationInput,
options: ExecutionOptions = {},
): Promise<Result<Evidence, AnalysisError>> => {
const authorized = await authorize(authority, input);
if (!authorized.ok) return authorized;
if (provider === undefined)
return err(
new AnalysisCapabilityUnavailableError(
PROVIDER_ID,
OPERATION,
"active Electron observation provider is not configured",
),
);
const captured = await provider.capture(authorized.value, options);
return captured.ok
? {
ok: true,
value: createElectronActiveEvidence(
authorized.value,
captured.value,
provider.identity(),
),
}
: captured;
};

View File

@ -333,10 +333,6 @@ const completeCapture = async (options: {
}
throw processCaptureCancelled();
}
if (runtime.reactive !== undefined) {
await runtime.reactive.coordinator.submit({ kind: "target_lost" });
runtime.reactive.unsubscribe();
}
const settlement = await observeSettlement(
options.runId,
selectCapturedProcessGroupIds(runtime.terminal.pid, options.samples),
@ -345,6 +341,15 @@ const completeCapture = async (options: {
);
runtime.checkpoints.trigger("settled");
const samplingPartial = (await runtime.stopSampler()).partial;
if (runtime.reactive !== undefined) {
// Process, filesystem, shim, and protocol observations can be recorded
// during settlement after the PTY exits. Make target loss terminal only
// after that journal has drained, otherwise the coordinator's control
// priority discards a valid multi-source completion predicate.
await runtime.reactive.coordinator.drain();
await runtime.reactive.coordinator.submit({ kind: "target_lost" });
runtime.reactive.unsubscribe();
}
assertNotCancelled(options.signal);
const after = await snapshotRoots(scenario, options.signal);
const renderedFrames = await runtime.renderer.frames();

View File

@ -71,9 +71,10 @@ export const normalizeProcessSamples = (
"<no-temporary-root>",
rootPid,
);
return scenario.normalization.pids
const withNormalizedPids = scenario.normalization.pids
? normalizePidTokens(normalized, identifiers)
: normalized;
return withNormalizedPids.trim();
};
return samples.map((sample) => ({
at_ms: normalizeProcessElapsedTime(

View File

@ -25,6 +25,7 @@ const grantSchema = z.object({
"browser_observe",
"browser_automate",
"electron_observe",
"electron_automate",
"evidence_read",
"evidence_write",
"investigation_input",

View File

@ -3,16 +3,19 @@ import { mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
windowsNativeCapabilities,
type WindowsNativeCapabilitySet,
} from "../process/WindowsAuthority.js";
import {
probeProcessCaptureCapability,
type ProcessCaptureCapability,
} from "./ProcessCaptureCapability.js";
/** One named Windows host observation without inferred authority. */
interface WindowsCapabilityOutcome {
readonly available: boolean;
readonly reason: string | null;
}
export type WindowsCapabilityOutcome =
| { readonly available: true; readonly reason: null }
| { readonly available: false; readonly reason: string };
/** Injectable host probes used by CI and deterministic unit tests. */
export interface WindowsCapabilityDependencies {
@ -36,6 +39,8 @@ export interface WindowsCapabilityReport {
unix_domain_socket: WindowsCapabilityOutcome;
pty: WindowsCapabilityOutcome;
}>;
/** Security controls that are unavailable until native authority exists. */
readonly security: WindowsNativeCapabilitySet;
}
/** Probe named prerequisites without converting missing controls into support. */
@ -54,11 +59,11 @@ export const probeWindowsCapabilities = async (
no_follow_open: dependencies.probeNoFollowOpen(),
private_acl: dependencies.probePrivateAcl(),
unix_domain_socket: dependencies.probeUnixDomainSocket(),
pty: {
available: pty.available,
reason: pty.available ? null : pty.reason,
},
pty: pty.available
? { available: true, reason: null }
: { available: false, reason: pty.reason },
},
security: windowsNativeCapabilities(dependencies.platform),
};
};

View File

@ -58,3 +58,14 @@ export const authorizeRuntimeLocation = async (
},
};
};
/** Authorize a target whose Node Inspector URL omits its main entry path. */
export const authorizeRuntimeTargetLocation = async (
value: string,
targetType: string,
roots: readonly string[],
origins: readonly string[],
): Promise<RuntimeLocationDecision> =>
// A bare file:// URL contains no source identity. Do not attach to the
// first configured root merely because the target happens to be Node.
authorizeRuntimeLocation(value, roots, origins);

View File

@ -0,0 +1,489 @@
import type { ElectronApplication, Page } from "playwright-core";
import { z } from "zod";
import type { ExecutionOptions } from "../application/AnalysisProvider.js";
import { BrowserObservationError } from "../domain/errors.js";
import { redactSensitiveText } from "./SensitiveTextCapture.js";
import type {
ElectronActiveObservationInput,
ElectronActiveObservationResult,
} from "../domain/electronActiveObservation.js";
const OPERATION = "capture_electron_scenario" as const;
const hookEventSchema = z.strictObject({
sequence: z.number().int().min(1),
correlation_id: z.string().max(256).nullable(),
kind: z.enum([
"main-handler-invocation",
"main-event-invocation",
"utility-process-fork",
"utility-process-message",
"ipc-main-to-renderer",
"ipc-utility-to-main",
"ipc-renderer-send",
"ipc-renderer-invoke",
"ipc-renderer-post-message",
"app-lifecycle",
"window-lifecycle",
"web-contents-lifecycle",
"navigation",
"shell-attempt",
"process-lifecycle",
"permission",
"popup-attempt",
"download",
"protocol",
"preload",
"native-addon",
"updater",
"error",
]),
event: z.string().max(128).nullable(),
phase: z.enum(["attempted", "completed", "blocked", "failed", "observed"]),
channel: z.string().max(1_024).nullable(),
channel_truncated: z.boolean().default(false),
direction: z
.enum([
"renderer-to-main",
"main-to-renderer",
"main-to-utility",
"utility-to-main",
])
.nullable(),
sender: z.string().max(256).nullable(),
receiver: z.string().max(256).nullable(),
frame: z.string().max(256).nullable(),
target: z.string().max(256).nullable(),
argument_shapes: z.array(z.string().max(64)).max(32),
argument_shapes_truncated: z.boolean().default(false),
result_shape: z.string().max(64).nullable(),
process_type: z.string().max(128).nullable(),
source: z.string().max(64),
capture_method: z.enum(["api-wrapper", "event-emitter", "process-hook"]),
artifact_path: z.string().max(16_384).nullable(),
artifact_sha256: z
.string()
.regex(/^[a-f0-9]{64}$/u)
.nullable(),
error: z.boolean(),
});
const hookSnapshotSchema = z.strictObject({
events: z.array(hookEventSchema).max(20_000),
observed: z.number().int().min(0),
observed_ipc: z.number().int().min(0),
observed_runtime: z.number().int().min(0),
truncated: z.boolean(),
hook_error: z.boolean(),
});
const metricSchema = z.strictObject({
pid: z.number().int().min(1),
type: z.string().min(1).max(128),
name: z.string().max(256).nullable(),
service_name: z.string().max(256).nullable(),
});
const windowMetadataSchema = z.strictObject({
window_id: z.string().max(256),
web_contents_id: z.string().max(256),
visible: z.boolean().nullable(),
destroyed: z.boolean(),
});
type ElectronAction = ElectronActiveObservationInput["actions"][number];
type ElectronActions = ElectronActiveObservationResult["actions"];
export type ElectronHookSnapshot = z.infer<typeof hookSnapshotSchema>;
export type ElectronHookEvent = z.infer<typeof hookEventSchema>;
export type ElectronMetrics = z.infer<typeof metricSchema>[];
type ElectronWindowMetadata = z.infer<typeof windowMetadataSchema>;
type ElectronPageSnapshot = {
readonly index: number;
readonly url: string;
readonly title: string;
};
/** Run bounded, explicit actions against provider-owned Electron windows. */
export const runElectronActions = async (
application: ElectronApplication,
input: ElectronActiveObservationInput,
options: ExecutionOptions,
deadline: number,
): Promise<ElectronActions> => {
const actions: ElectronActions = [];
for (const action of input.actions) {
if (options.signal?.aborted === true)
throw new BrowserObservationError(OPERATION, "cancelled");
const actionStartedAt = Date.now();
const windowIndex = actionWindowIndex(action);
let selectedWindow: Page | undefined;
try {
const remaining = deadline - Date.now();
if (remaining <= 0)
throw new BrowserObservationError(OPERATION, "timeout");
selectedWindow =
windowIndex === null
? undefined
: await waitForWindow({
application,
windowIndex,
timeout: input.limits.action_timeout_ms,
signal: options.signal,
deadline,
});
if (
(action.kind === "click" ||
action.kind === "renderer-reload" ||
action.kind === "renderer-crash") &&
selectedWindow === undefined
)
throw new BrowserObservationError(OPERATION, "window_not_found");
await runAction({
application,
action,
selectedWindow,
remaining,
actionTimeoutMs: input.limits.action_timeout_ms,
options,
deadline,
});
actions.push({
step_id: action.step_id,
kind: action.kind,
window_index: windowIndex,
target:
selectedWindow === undefined ? null : `window:${String(windowIndex)}`,
status: "completed",
elapsed_ms: Date.now() - actionStartedAt,
error: null,
});
} catch (cause: unknown) {
actions.push({
step_id: action.step_id,
kind: action.kind,
window_index: windowIndex,
target:
selectedWindow === undefined ? null : `window:${String(windowIndex)}`,
status: options.signal?.aborted ? "cancelled" : "failed",
elapsed_ms: Date.now() - actionStartedAt,
error: safeActionErrorMessage(cause, action),
});
break;
}
}
return actions;
};
type RunActionContext = {
readonly application: ElectronApplication;
readonly action: ElectronAction;
readonly selectedWindow: Page | undefined;
readonly remaining: number;
readonly actionTimeoutMs: number;
readonly options: ExecutionOptions;
readonly deadline: number;
};
const runAction = async ({
application,
action,
selectedWindow,
remaining,
actionTimeoutMs,
options,
deadline,
}: RunActionContext): Promise<void> => {
switch (action.kind) {
case "click":
if (selectedWindow === undefined)
throw new BrowserObservationError(OPERATION, "window_not_found");
await runWithExecutionLimits(
selectedWindow.locator(action.selector).click({
timeout: Math.min(actionTimeoutMs, remaining),
}),
options.signal,
deadline,
);
return;
case "wait": {
const page =
selectedWindow ??
(await application.firstWindow({
timeout: Math.min(actionTimeoutMs, remaining),
}));
await runWithExecutionLimits(
page.waitForTimeout(Math.min(action.duration_ms, remaining)),
options.signal,
deadline,
);
if (action.duration_ms > remaining)
throw new BrowserObservationError(OPERATION, "timeout");
return;
}
case "renderer-reload":
if (selectedWindow === undefined)
throw new BrowserObservationError(OPERATION, "window_not_found");
await runWithExecutionLimits(
selectedWindow.reload({
timeout: Math.min(actionTimeoutMs, remaining),
}),
options.signal,
deadline,
);
return;
case "renderer-crash": {
const crashed = await runWithExecutionLimits(
application.evaluate(({ BrowserWindow }, index) => {
const candidate = BrowserWindow.getAllWindows()[index];
if (candidate === undefined || candidate.isDestroyed()) return false;
candidate.webContents.forcefullyCrashRenderer();
return true;
}, action.window_index),
options.signal,
deadline,
);
if (!crashed)
throw new BrowserObservationError(OPERATION, "window_not_found");
return;
}
case "deep-link":
await runWithExecutionLimits(
emitDeepLink(application, action),
options.signal,
deadline,
);
return;
}
};
const actionWindowIndex = (action: ElectronAction): number | null => {
switch (action.kind) {
case "click":
case "renderer-reload":
case "renderer-crash":
return action.window_index;
case "wait":
return action.window_index ?? null;
case "deep-link":
return null;
}
};
type WaitForWindowContext = {
readonly application: ElectronApplication;
readonly windowIndex: number;
readonly timeout: number;
readonly signal: AbortSignal | undefined;
readonly deadline: number;
};
const waitForWindow = async ({
application,
windowIndex,
timeout,
signal,
deadline,
}: WaitForWindowContext): Promise<Page> => {
const stopAt = Math.min(deadline, Date.now() + timeout);
while (Date.now() < stopAt) {
if (signal?.aborted === true)
throw new BrowserObservationError(OPERATION, "cancelled");
const window = application.windows()[windowIndex];
if (window !== undefined) return window;
await runWithExecutionLimits(
new Promise<void>((resolve) => setTimeout(resolve, 25)),
signal,
stopAt,
);
}
throw new BrowserObservationError(OPERATION, "window_not_found");
};
const emitDeepLink = (
application: ElectronApplication,
action: Extract<ElectronAction, { kind: "deep-link" }>,
): Promise<boolean> =>
application.evaluate(
({ app }, payload) => {
if (payload.delivery === "open-url")
return app.emit(
"open-url",
{ preventDefault: () => undefined },
payload.url,
);
return app.emit("second-instance", {}, [payload.url], "", {});
},
{ delivery: action.delivery, url: action.url },
);
/** Read the current Electron windows, metrics, and bounded hook snapshot. */
// oxlint-disable-next-line max-lines-per-function -- one state read must share one capture boundary.
export const readApplicationState = async (
application: ElectronApplication,
maxWindows: number,
): Promise<{
readonly windows: ReadonlyArray<{
readonly window_id: string;
readonly web_contents_id: string;
readonly url: string;
readonly title: string;
readonly visible: boolean | null;
readonly destroyed: boolean;
}>;
readonly windowsTruncated: boolean;
readonly metrics: ElectronMetrics;
readonly electronVersion: string;
readonly hookSnapshot: ElectronHookSnapshot;
}> => {
const pages = application.windows();
const pageSnapshotsPromise: Promise<ElectronPageSnapshot[]> = Promise.all(
pages.slice(0, maxWindows).map(
async (page: Page, index): Promise<ElectronPageSnapshot> => ({
index,
url: page.url().slice(0, 65_536),
title: (await page.title()).slice(0, 16_384),
}),
),
);
const windowMetadataPromise: Promise<ElectronWindowMetadata[]> = application
.evaluate(({ BrowserWindow }) =>
BrowserWindow.getAllWindows().map(
(window: {
readonly id: number;
readonly webContents: { readonly id: number };
isDestroyed(): boolean;
isVisible(): boolean;
}) => ({
window_id: `window:${String(window.id)}`,
web_contents_id: `webContents:${String(window.webContents.id)}`,
visible: window.isDestroyed() ? null : window.isVisible(),
destroyed: window.isDestroyed(),
}),
),
)
.then((value) => z.array(windowMetadataSchema).parse(value));
const processMetricsPromise: Promise<ElectronMetrics> = application
.evaluate(({ app }) =>
app
.getAppMetrics()
.map(
(metric: {
readonly pid: number;
readonly type: string;
readonly name?: string;
readonly serviceName?: string;
}) => ({
pid: metric.pid,
type: metric.type,
name: metric.name ?? null,
service_name: metric.serviceName ?? null,
}),
),
)
.then((value) => z.array(metricSchema).parse(value));
const electronVersionPromise: Promise<string> = application
.evaluate(() => process.versions.electron)
.then((value) => z.string().parse(value));
const hookSnapshotPromise: Promise<ElectronHookSnapshot> = application
.evaluate(() => {
const candidate = Reflect.get(globalThis, "__reaElectronActiveSnapshot");
return typeof candidate === "function"
? candidate()
: {
events: [],
observed: 0,
observed_ipc: 0,
observed_runtime: 0,
truncated: false,
hook_error: true,
};
})
.then((value) => hookSnapshotSchema.parse(value));
const [
pageSnapshots,
windowMetadata,
processMetrics,
electronVersion,
hookSnapshot,
] = await Promise.all([
pageSnapshotsPromise,
windowMetadataPromise,
processMetricsPromise,
electronVersionPromise,
hookSnapshotPromise,
]);
return {
windows: pageSnapshots.map((page) => {
const metadata = windowMetadata[page.index];
if (metadata === undefined)
throw new BrowserObservationError(OPERATION, "window_metadata_missing");
return {
window_id: metadata.window_id,
web_contents_id: metadata.web_contents_id,
url: page.url,
title: page.title,
visible: metadata.visible,
destroyed: metadata.destroyed,
};
}),
windowsTruncated: pages.length > maxWindows,
metrics: processMetrics,
electronVersion,
hookSnapshot,
};
};
/** Apply a deadline and caller cancellation to one Playwright operation. */
export const runWithExecutionLimits = async <Value>(
operation: Promise<Value>,
signal: AbortSignal | undefined,
deadline: number,
): Promise<Value> => {
if (signal?.aborted === true)
throw new BrowserObservationError(OPERATION, "cancelled");
let timer: ReturnType<typeof setTimeout> | undefined;
let abortListener: (() => void) | undefined;
const timeout = new Promise<never>((_, reject) => {
timer = setTimeout(
() => reject(new BrowserObservationError(OPERATION, "timeout")),
Math.max(1, deadline - Date.now()),
);
});
const cancellation =
signal === undefined
? timeout
: Promise.race([
timeout,
new Promise<never>((_, reject) => {
abortListener = () =>
reject(new BrowserObservationError(OPERATION, "cancelled"));
signal.addEventListener("abort", abortListener, { once: true });
}),
]);
try {
return await Promise.race([operation, cancellation]);
} finally {
if (timer !== undefined) clearTimeout(timer);
if (abortListener !== undefined)
signal?.removeEventListener("abort", abortListener);
}
};
const safeActionErrorMessage = (
cause: unknown,
action: ElectronAction,
): string => {
let message =
cause instanceof Error ? cause.message : "Electron action failed";
const inputValues = Object.values(action)
.filter(
(value): value is string => typeof value === "string" && value.length > 0,
)
.sort((left, right) => right.length - left.length);
for (const value of inputValues)
message = message.split(value).join("<redacted-input>");
return redactSensitiveText(message).slice(0, 1_024);
};

View File

@ -0,0 +1,432 @@
import { realpath } from "node:fs/promises";
import { randomUUID } from "node:crypto";
import { fileURLToPath } from "node:url";
import { _electron as electron } from "playwright-core";
import type {
ExecutionOptions,
ProviderIdentity,
} from "../application/AnalysisProvider.js";
import type { ElectronActiveObservationPort } from "../application/ElectronActiveObservationPort.js";
import {
electronActiveObservationResultSchema,
type ElectronActiveObservationInput,
type ElectronActiveObservationResult,
} from "../domain/electronActiveObservation.js";
import {
AnalysisError,
BrowserObservationError,
ProviderAdapterError,
} from "../domain/errors.js";
import { isPathContained } from "../domain/permissionPolicy.js";
import { err, ok, type Result } from "../domain/result.js";
import {
cleanupOwnedProcessGroup,
cleanupWindowsProcessTree,
observeOwnedProcessLineage,
selectCapturedProcessGroupIds,
verifyNoTokenOwnedProcesses,
type OwnedProcessGroup,
type ProcessCleanupResult,
type ProcessLineageObservation,
} from "../process/ProcessOwnership.js";
import {
runElectronActions,
readApplicationState,
runWithExecutionLimits,
type ElectronHookEvent,
type ElectronHookSnapshot,
type ElectronMetrics,
} from "./PlaywrightElectronActiveActions.js";
const OPERATION = "capture_electron_scenario" as const;
/** Public identity for provider-owned Electron runtime experiments. */
export const PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY: ProviderIdentity =
Object.freeze({
id: "rea-playwright-electron-active",
name: "REA Playwright active Electron observation provider",
version: "1",
});
const hookPath = fileURLToPath(
new URL("../../scripts/electron-active-hook.cjs", import.meta.url),
);
type ElectronApplication = Awaited<ReturnType<typeof electron.launch>>;
type ElectronPaths = {
readonly executable: string;
readonly application: string;
readonly root: string;
};
const observableEventFamilies = [
"app-lifecycle",
"window-lifecycle",
"web-contents-lifecycle",
"navigation",
"shell-attempt",
"process-lifecycle",
"permission",
"popup-attempt",
"download",
"protocol",
"preload",
"preload-configuration",
"renderer-ipc",
"native-addon",
"updater",
"error",
"ipc",
] as const;
const ipcEventKinds = new Set<ElectronHookEvent["kind"]>([
"main-handler-invocation",
"main-event-invocation",
"utility-process-fork",
"utility-process-message",
"ipc-main-to-renderer",
"ipc-utility-to-main",
"ipc-renderer-send",
"ipc-renderer-invoke",
"ipc-renderer-post-message",
]);
const createCoverage = (hookSnapshot: ElectronHookSnapshot) => {
const observedEventFamilies: string[] = [
...new Set(
hookSnapshot.events.map(({ kind, process_type }) => {
if (kind === "preload" && process_type === "main")
return "preload-configuration";
if (kind.startsWith("ipc-renderer") && process_type !== "renderer")
return "ipc";
return ipcEventKinds.has(kind) ? "ipc" : kind;
}),
),
].sort();
const unavailableEventFamilies = new Set(
observableEventFamilies.filter(
(family) => !observedEventFamilies.includes(family),
),
);
const observedRoles = [
...new Set(
hookSnapshot.events.flatMap(({ process_type, kind }) => [
...(process_type === null ? [] : [process_type]),
...(kind === "window-lifecycle" ? ["window"] : []),
...(kind === "web-contents-lifecycle" || kind === "navigation"
? ["web_contents"]
: []),
...(kind === "preload" && process_type === "preload"
? ["preload"]
: []),
...(kind.startsWith("ipc-renderer") && process_type === "renderer"
? ["renderer"]
: []),
]),
),
].sort();
const rendererContextObserved = hookSnapshot.events.some(
({ process_type }) =>
process_type === "renderer" || process_type === "preload",
);
if (!rendererContextObserved) {
unavailableEventFamilies.add("preload");
unavailableEventFamilies.add("renderer-ipc");
}
return {
status: hookSnapshot.hook_error
? "hook_conflict"
: hookSnapshot.truncated
? "capture_truncated"
: "partial_attach",
observed_event_families: observedEventFamilies,
unavailable_event_families: [...unavailableEventFamilies].sort(),
observed_roles: observedRoles,
pre_capture_activity: "unavailable",
} as const;
};
/** Launch an owned Electron application through the official Playwright API. */
export class PlaywrightElectronActiveProvider
implements ElectronActiveObservationPort
{
identity(): ProviderIdentity {
return PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY;
}
async capture(
input: ElectronActiveObservationInput,
options: ExecutionOptions = {},
): Promise<Result<ElectronActiveObservationResult, AnalysisError>> {
let application: ElectronApplication | undefined;
let ownership: OwnedProcessGroup | undefined;
let lineage: ProcessLineageObservation | undefined;
let outcome: Result<ElectronActiveObservationResult, AnalysisError>;
const runId = randomUUID();
try {
if (options.signal?.aborted === true)
throw new BrowserObservationError(OPERATION, "cancelled");
const deadline = Date.now() + input.limits.max_duration_ms;
const paths = await canonicalPaths(input);
application = await electron.launch({
executablePath: paths.executable,
cwd: paths.root,
env: safeElectronEnvironment(
input.limits.max_ipc_events,
input.limits.max_runtime_events,
runId,
),
args: ["-r", hookPath, paths.application, ...input.args],
timeout: Math.max(1, deadline - Date.now()),
});
const leaderPid = application.process().pid;
if (
typeof leaderPid !== "number" ||
!Number.isSafeInteger(leaderPid) ||
leaderPid <= 0
)
throw new BrowserObservationError(
OPERATION,
"process_ownership_unavailable",
);
ownership = {
runId,
leaderPid,
processGroupId: leaderPid,
expectedParentPid: process.pid,
expectedCommand: paths.executable,
sweepTokenOwnedProcesses: true,
};
const actions = await runElectronActions(
application,
input,
options,
deadline,
);
const state = await runWithExecutionLimits(
readApplicationState(application, input.limits.max_windows),
options.signal,
deadline,
);
outcome = ok(createResult(paths, input, actions, state));
} catch (cause: unknown) {
outcome = err(providerError(cause));
}
if (application !== undefined) {
if (ownership !== undefined)
lineage = await observeOwnedProcessLineage(ownership);
let closeError: unknown;
try {
await runWithExecutionLimits(
application.close(),
undefined,
Date.now() + 5_000,
);
} catch (cause: unknown) {
closeError = cause;
}
const cleanup =
ownership === undefined
? ({
cleaned: false,
reason: "owned Electron process identity was unavailable",
} satisfies ProcessCleanupResult)
: await cleanupElectronProcesses(ownership, lineage);
if (!cleanup.cleaned)
return err(
providerError(
new BrowserObservationError(OPERATION, "cleanup_failed", {
cause: new Error(cleanup.reason),
}),
),
);
if (closeError !== undefined) return err(providerError(closeError));
}
return outcome;
}
}
const cleanupElectronProcesses = async (
ownership: OwnedProcessGroup,
lineage: ProcessLineageObservation | undefined,
): Promise<ProcessCleanupResult> => {
if (process.platform === "win32") {
const root = await cleanupWindowsProcessTree(ownership.leaderPid);
if (!root.cleaned) return root;
if (lineage?.status !== "verified")
return {
cleaned: false,
reason:
"owned Electron lineage was unavailable; helper cleanup was not proven",
};
for (const descendant of lineage.lineage.descendants) {
const result = await cleanupWindowsProcessTree(descendant.pid);
if (!result.cleaned) return result;
}
return root;
}
if (lineage?.status !== "verified") {
const root = await cleanupOwnedProcessGroup(ownership);
return root.cleaned
? {
cleaned: false,
reason:
"owned Electron lineage was unavailable; helper cleanup was not proven",
}
: root;
}
const groupIds = selectCapturedProcessGroupIds(
ownership.leaderPid,
lineage.lineage.descendants.map(({ pid, processGroupId }) => ({
pid,
process_group_id: processGroupId,
})),
);
let signaled = false;
for (const processGroupId of groupIds) {
const cleanupOwnership: OwnedProcessGroup =
processGroupId === ownership.processGroupId
? { ...ownership, leaderPid: processGroupId, processGroupId }
: {
runId: ownership.runId,
leaderPid: processGroupId,
processGroupId,
};
const result = await cleanupOwnedProcessGroup(cleanupOwnership);
if (!result.cleaned) return result;
signaled ||= result.signaled;
}
const remaining = await verifyNoTokenOwnedProcesses(ownership.runId);
if (!remaining.cleaned) return remaining;
return { cleaned: true, signaled };
};
const createResult = (
paths: ElectronPaths,
input: ElectronActiveObservationInput,
actions: ElectronActiveObservationResult["actions"],
state: {
readonly windows: ReadonlyArray<{
readonly window_id: string;
readonly web_contents_id: string;
readonly url: string;
readonly title: string;
readonly visible: boolean | null;
readonly destroyed: boolean;
}>;
readonly windowsTruncated: boolean;
readonly metrics: ElectronMetrics;
readonly electronVersion: string;
readonly hookSnapshot: ElectronHookSnapshot;
},
): ElectronActiveObservationResult => {
const { hookSnapshot } = state;
const ipcEvents = hookSnapshot.events.filter(({ kind }) =>
ipcEventKinds.has(kind),
);
return electronActiveObservationResultSchema.parse({
schema_version: 1,
application: {
executable_path: paths.executable,
application_path: paths.application,
electron_version: state.electronVersion,
process_ownership: "provider-owned",
cleanup: "terminated-owned-process",
},
actions,
windows: state.windows.slice(0, input.limits.max_windows),
windows_truncated: state.windowsTruncated,
processes: {
items: state.metrics.slice(0, input.limits.max_processes),
truncated: state.metrics.length > input.limits.max_processes,
},
ipc: {
events: ipcEvents.slice(0, input.limits.max_ipc_events),
observed: hookSnapshot.observed_ipc,
retained: Math.min(ipcEvents.length, input.limits.max_ipc_events),
truncated:
hookSnapshot.truncated ||
ipcEvents.length > input.limits.max_ipc_events,
},
timeline: {
events: hookSnapshot.events.slice(0, input.limits.max_runtime_events),
observed: hookSnapshot.observed,
retained: Math.min(
hookSnapshot.events.length,
input.limits.max_runtime_events,
),
truncated:
hookSnapshot.truncated ||
hookSnapshot.events.length > input.limits.max_runtime_events,
},
coverage: createCoverage(hookSnapshot),
limitations: [
"IPC payloads are represented only by bounded value shapes; values are never retained, channels are capped at 1,024 characters, and argument-shape arrays at 32 entries.",
"IPC direction and sender/receiver identifiers are observed only where Electron exposes them at the hooked boundary.",
"The runtime timeline records bounded lifecycle, navigation, shell, permission, popup, download, protocol, preload, native-addon, process, and IPC events; activity before hook installation is unavailable.",
"The preload and renderer process contexts are not instrumented by the main-process -r hook; preload configuration and contextBridge API-shape events are main-boundary observations, not proof of renderer-side execution.",
"Process metrics are an Electron API snapshot and do not prove hostile-local-user isolation.",
"External shell opens, external navigation, permission grants, downloads, popup windows, updater relaunches, and OS integration are blocked and recorded by the active hook; other application filesystem and network behavior is not sandboxed by this provider.",
...(state.windowsTruncated
? ["Windows beyond max_windows were not retained."]
: []),
...(hookSnapshot.hook_error
? ["The active IPC hook could not be installed."]
: []),
],
});
};
const canonicalPaths = async (
input: ElectronActiveObservationInput,
): Promise<ElectronPaths> => {
const [executable, application, root] = await Promise.all([
realpath(input.executable_path),
realpath(input.application_path),
realpath(input.application_root),
]);
if (!isPathContained(root, application))
throw new BrowserObservationError(OPERATION, "target_not_allowed");
return { executable, application, root };
};
const safeElectronEnvironment = (
maxIpcEvents: number,
maxRuntimeEvents: number,
runId: string,
): Record<string, string> =>
Object.fromEntries([
...[
"HOME",
"LANG",
"PATH",
"TMP",
"TMPDIR",
"USER",
"DISPLAY",
"WAYLAND_DISPLAY",
]
.map((name) => [name, process.env[name]] as const)
.filter(
(entry): entry is readonly [string, string] => entry[1] !== undefined,
),
["REA_ELECTRON_ACTIVE_MAX_IPC_EVENTS", String(maxIpcEvents)] as const,
[
"REA_ELECTRON_ACTIVE_MAX_RUNTIME_EVENTS",
String(maxRuntimeEvents),
] as const,
["REA_PROCESS_RUN_ID", runId] as const,
]);
const providerError = (cause: unknown): AnalysisError =>
cause instanceof AnalysisError
? cause
: new ProviderAdapterError(
PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY.id,
OPERATION,
{
cause,
},
);

View File

@ -1,5 +1,5 @@
const SECRET_ASSIGNMENT =
/\b(authorization|api[_-]?key|access[_-]?token|refresh[_-]?token|password|passwd|secret|cookie|session)\b\s*[:=]\s*(?:Bearer\s+[A-Za-z0-9._~+/=-]+|"[^"]*"|'[^']*'|[^\s,;]+)/giu;
/\b(authorization|api[_-]?key|access[_-]?token|refresh[_-]?token|token|password|passwd|secret|cookie|session)\b\s*[:=]\s*(?:Bearer\s+[A-Za-z0-9._~+/=-]+|"[^"]*"|'[^']*'|[^\s,;]+)/giu;
const BEARER = /\bBearer\s+[A-Za-z0-9._~+/=-]+/giu;
const JWT = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/gu;

View File

@ -30,6 +30,7 @@ export const describeInspectorTargetLimitations = (): string[] => [
"REA attaches to an already-running exact target and never launches, resumes, evaluates, pauses, or mutates it.",
"Only Runtime.enable and Debugger.enable are sent; source text, object values, EventEmitter activity, and Electron IPC are not inspected.",
"Target IDs and locations are authorized, but the Inspector protocol does not authenticate an operating-system process ID or Electron role.",
"Some Electron main targets report only file://; for those targets, the first approved file root is recorded as a scope fallback and script locations are authorized independently.",
"No runtime graph depth is traversed because passive Inspector events do not establish require/import caller edges.",
];

View File

@ -27,7 +27,7 @@ import {
} from "./CdpCaptureValues.js";
import { CdpConnection, type CdpEvent } from "./CdpConnection.js";
import {
authorizeRuntimeLocation,
authorizeRuntimeTargetLocation,
canonicalRuntimeRoots,
} from "./JavaScriptRuntimeScope.js";
import {
@ -95,8 +95,9 @@ export class V8InspectorProvider implements JavaScriptRuntimeObservationPort {
const allowed: AuthorizedV8InspectorTarget[] = [];
const excluded = createInspectorExclusionCounts();
for (const target of discovery.targets) {
const decision = await authorizeRuntimeLocation(
const decision = await authorizeRuntimeTargetLocation(
target.url,
target.type,
roots,
input.allowed_origins,
);
@ -202,8 +203,9 @@ const authorizedTarget = async (
"observe_javascript_runtime",
"target_not_found",
);
const decision = await authorizeRuntimeLocation(
const decision = await authorizeRuntimeTargetLocation(
target.url,
target.type,
roots,
input.allowed_origins,
);

View File

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

View File

@ -4,15 +4,18 @@ import {
inspectElectronPage,
listElectronTargets,
} from "./application/ElectronObservationService.js";
import { captureElectronScenario } from "./application/ElectronActiveObservationService.js";
import { reconcileJavaScriptRuntimeEvidence } from "./application/JavaScriptRuntimeReconciliationService.js";
import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js";
import { CdpElectronProvider } from "./browser/CdpElectronProvider.js";
import { PlaywrightElectronActiveProvider } from "./browser/PlaywrightElectronActiveProvider.js";
import { logCliCommand } from "./cliLogging.js";
import { parseConfig } from "./config.js";
import {
inspectElectronPageInputSchema,
listElectronTargetsInputSchema,
} from "./domain/electronObservation.js";
import { electronActiveObservationInputSchema } from "./domain/electronActiveObservation.js";
import { AnalysisInputError, projectAnalysisError } from "./domain/errors.js";
import type { JsonValue } from "./domain/jsonValue.js";
import type { Logger } from "./logger.js";
@ -38,10 +41,47 @@ export const registerElectronCommands = (
logger: Logger,
): void => {
registerElectronObservationCommands(cli, logger);
registerElectronActiveCommand(cli, logger);
registerJavaScriptApplicationCommand(cli, logger);
registerJavaScriptRuntimeReconciliationCommand(cli, logger);
};
const registerElectronActiveCommand = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
): void => {
cli.command(CLI_COMMANDS.captureElectronScenario, {
description: "Run one approved, bounded owned Electron scenario",
args: z.object({
inputJson: z
.string()
.describe(
"Inline JSON or a JSON file matching capture_electron_scenario",
),
}),
run: ({ args }) =>
logCliCommand(logger, CLI_COMMANDS.captureElectronScenario, async () => {
const input = await parseCliJsonInput(
args.inputJson,
"capture_electron_scenario",
);
if (!input.ok) return input.error;
const parsed = electronActiveObservationInputSchema.safeParse(
input.value,
);
if (!parsed.success) return inputError("capture_electron_scenario");
const context = await electronContext();
if (!context.ok) return context.error;
const result = await captureElectronScenario(
context.activeProvider,
context.authority,
parsed.data,
);
return result.ok ? result.value : cliError(result.error);
}),
});
};
const registerJavaScriptRuntimeReconciliationCommand = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
@ -227,6 +267,7 @@ const electronContext = async () => {
ok: true as const,
authority: authority.value,
provider: new CdpElectronProvider(),
activeProvider: new PlaywrightElectronActiveProvider(),
allowedFileRoots: config.value.electronFileRoots,
};
};

View File

@ -21,6 +21,7 @@ const capabilitySchema = z.enum([
"browser_observe",
"browser_automate",
"electron_observe",
"electron_automate",
"evidence_read",
"evidence_write",
"investigation_input",

View File

@ -0,0 +1,63 @@
import { isAbsolute } from "node:path";
import type { PermissionCeiling } from "../domain/permissionPolicy.js";
import { ConfigurationError } from "../domain/errors.js";
import { err, ok, type Result } from "../domain/result.js";
import { permissionScope } from "./permissions.js";
import { parseStringArray } from "./parsers.js";
import type { Environment } from "./environment.js";
export interface ElectronAutomationArrays {
readonly electronAutomationExecutableRoots: readonly string[];
readonly electronAutomationApplicationRoots: readonly string[];
}
/** Parse the exact filesystem roots admitted for active Electron automation. */
export const parseElectronAutomationArrays = (
env: Environment,
): Result<ElectronAutomationArrays, ConfigurationError> => {
const executableRoots = parseAbsoluteRoots(
env.REA_ELECTRON_AUTOMATE_EXECUTABLE_ROOTS_JSON,
"REA_ELECTRON_AUTOMATE_EXECUTABLE_ROOTS_JSON",
);
if (!executableRoots.ok) return executableRoots;
const applicationRoots = parseAbsoluteRoots(
env.REA_ELECTRON_AUTOMATE_APPLICATION_ROOTS_JSON,
"REA_ELECTRON_AUTOMATE_APPLICATION_ROOTS_JSON",
);
if (!applicationRoots.ok) return applicationRoots;
return ok({
electronAutomationExecutableRoots: executableRoots.value,
electronAutomationApplicationRoots: applicationRoots.value,
});
};
/** Add the separately permissioned, no-network Electron automation ceiling. */
export const appendElectronAutomationCeiling = (
ceilings: PermissionCeiling[],
env: Environment,
arrays: ElectronAutomationArrays,
): void => {
if (env.REA_ELECTRON_AUTOMATE_ENABLED !== "true") return;
ceilings.push(
permissionScope(
"electron_automate",
arrays.electronAutomationApplicationRoots,
{
executables: arrays.electronAutomationExecutableRoots,
network: "external",
},
),
);
};
const parseAbsoluteRoots = (
value: string,
name: string,
): Result<readonly string[], ConfigurationError> => {
const parsed = parseStringArray(value, name);
if (!parsed.ok) return parsed;
return parsed.value.some((root) => !isAbsolute(root))
? err(new ConfigurationError(`${name} must encode absolute roots`))
: parsed;
};

View File

@ -63,6 +63,12 @@ const environmentSchema = z
REA_ELECTRON_OBSERVE_ENABLED: z.enum(["true", "false"]).default("false"),
REA_ELECTRON_CDP_ENDPOINTS_JSON: z.string().default("[]"),
REA_ELECTRON_FILE_ROOTS_JSON: z.string().default("[]"),
REA_ELECTRON_AUTOMATE_ENABLED: z.enum(["true", "false"]).default("false"),
REA_ELECTRON_AUTOMATE_AUTO_GRANT: z
.enum(["true", "false"])
.default("false"),
REA_ELECTRON_AUTOMATE_EXECUTABLE_ROOTS_JSON: z.string().default("[]"),
REA_ELECTRON_AUTOMATE_APPLICATION_ROOTS_JSON: z.string().default("[]"),
REA_V8_INSPECTOR_OBSERVE_ENABLED: z
.enum(["true", "false"])
.default("false"),

View File

@ -21,6 +21,11 @@ import {
browserEndpointSchema,
browserOriginSchema,
} from "../domain/browserObservation.js";
import {
appendElectronAutomationCeiling,
parseElectronAutomationArrays,
type ElectronAutomationArrays,
} from "./electronAutomation.js";
const DEFAULT_HOPPER_LAUNCHER_PATH =
"/Applications/Hopper Disassembler.app/Contents/MacOS/hopper";
@ -29,7 +34,7 @@ const defaultHopperLauncherPath = (): string =>
? "/opt/hopper/bin/Hopper"
: DEFAULT_HOPPER_LAUNCHER_PATH;
interface ParsedArrays {
interface ParsedArrays extends ElectronAutomationArrays {
readonly loaderArgs: readonly string[];
readonly executableRoots: readonly string[];
readonly workingRoots: readonly string[];
@ -116,6 +121,8 @@ const parseAllArrays = (
if (!secretPatterns.ok) return secretPatterns;
const observations = parseObservationArrays(env);
if (!observations.ok) return observations;
const electronAutomation = parseElectronAutomationArrays(env);
if (!electronAutomation.ok) return electronAutomation;
const javascriptReplayRoots = parseAbsoluteRoots(
env.REA_JAVASCRIPT_REPLAY_ROOTS_JSON,
"REA_JAVASCRIPT_REPLAY_ROOTS_JSON",
@ -137,6 +144,7 @@ const parseAllArrays = (
referenceRoots: referenceRoots.value,
secretPatterns: secretPatterns.value,
...observations.value,
...electronAutomation.value,
javascriptReplayRoots: javascriptReplayRoots.value,
managedRuntimeRoots: managedRuntimeRoots.value,
});
@ -378,6 +386,7 @@ const buildPermissionCeilings = (
appendBrowserObservationCeiling(ceilings, env, arrays);
appendBrowserScenarioCeiling(ceilings, env, arrays);
appendElectronObservationCeiling(ceilings, env, arrays);
appendElectronAutomationCeiling(ceilings, env, arrays);
appendV8InspectorObservationCeiling(ceilings, env, arrays);
appendNativeMountCeiling(ceilings, env);
appendJavaScriptReplayCeiling(ceilings, env, arrays);
@ -433,6 +442,11 @@ const buildAppConfig = (
electronObservationEnabled: env.REA_ELECTRON_OBSERVE_ENABLED === "true",
electronCdpEndpoints: arrays.electronEndpoints,
electronFileRoots: arrays.electronFileRoots,
electronAutomationPolicy: {
enabled: env.REA_ELECTRON_AUTOMATE_ENABLED === "true",
executableRoots: arrays.electronAutomationExecutableRoots,
applicationRoots: arrays.electronAutomationApplicationRoots,
},
v8InspectorObservationEnabled:
env.REA_V8_INSPECTOR_OBSERVE_ENABLED === "true",
v8InspectorEndpoints: arrays.v8InspectorEndpoints,
@ -459,7 +473,9 @@ const buildAppConfig = (
(capability !== "process_capture" ||
env.REA_PROCESS_CAPTURE_AUTO_GRANT === "true") &&
(capability !== "browser_automate" ||
env.REA_BROWSER_SCENARIO_AUTO_GRANT === "true"),
env.REA_BROWSER_SCENARIO_AUTO_GRANT === "true") &&
(capability !== "electron_automate" ||
env.REA_ELECTRON_AUTOMATE_AUTO_GRANT === "true"),
),
),
permissionProjectRoot: env.REA_PERMISSION_PROJECT_ROOT,

View File

@ -38,6 +38,11 @@ export interface AppConfig {
readonly electronObservationEnabled: boolean;
readonly electronCdpEndpoints: readonly string[];
readonly electronFileRoots: readonly string[];
readonly electronAutomationPolicy: {
readonly enabled: boolean;
readonly executableRoots: readonly string[];
readonly applicationRoots: readonly string[];
};
readonly v8InspectorObservationEnabled: boolean;
readonly v8InspectorEndpoints: readonly string[];
readonly v8InspectorFileRoots: readonly string[];

View File

@ -9,6 +9,10 @@ import {
inspectElectronPageInputSchema,
listElectronTargetsInputSchema,
} from "../domain/electronObservation.js";
import {
electronActiveObservationInputSchema,
electronActiveObservationResultSchema,
} from "../domain/electronActiveObservation.js";
import {
analyzeJavaScriptApplicationInputSchema,
javaScriptApplicationAnalysisResultV1Schema,
@ -113,6 +117,23 @@ const reconciliationOutputSchema = evidenceResultOf(
const endpoint = "http://127.0.0.1:9223";
const root = "/Applications/Example.app/Contents/Resources";
const activeExample = {
schema_version: 1,
executable_path: "/Applications/Electron.app/Contents/MacOS/Electron",
application_path: "/Applications/Example.app/Contents/Resources/main.js",
application_root: "/Applications/Example.app/Contents/Resources",
args: [],
actions: [{ step_id: "exercise-ipc", kind: "click", selector: "#run" }],
limits: {
max_duration_ms: 60_000,
action_timeout_ms: 5_000,
max_actions: 20,
max_ipc_events: 2_000,
max_processes: 32,
max_windows: 32,
},
approved: true,
};
/** Root-confined Electron file-page discovery and inspection contracts. */
export const ELECTRON_TOOL_CONTRACTS = [
@ -194,7 +215,7 @@ export const ELECTRON_TOOL_CONTRACTS = [
name: "reconcile_javascript_runtime",
...toolContractMetadata("reconcile_javascript_runtime"),
description:
"Reconcile verified static JavaScript application graphs with existing passive web or Electron CDP Evidence. Exact captured-source digests take priority over caller-declared file/URL mappings; target, frame, script, and worker ambiguity remains explicit, and source-map authority stays separate.",
"Reconcile verified static JavaScript application graphs with existing passive web/Electron CDP, passive V8 Inspector, or provider-owned active Electron Evidence. Active Electron captures contribute an explicitly partial target-only runtime record; they never invent renderer scripts, frames, workers, or execution claims. Exact captured-source digests take priority over caller-declared file/URL mappings; target, frame, script, and worker ambiguity remains explicit, and source-map authority stays separate.",
kind: "electron-provider",
inputSchema: reconcileJavaScriptRuntimeInputSchema,
outputSchema: reconciliationOutputSchema,
@ -205,4 +226,16 @@ export const ELECTRON_TOOL_CONTRACTS = [
},
],
},
{
name: "capture_electron_scenario",
...toolContractMetadata("capture_electron_scenario"),
description:
"Use this for an explicitly approved, provider-owned Electron run when passive CDP or Inspector observation cannot exercise application behavior. REA owns startup and teardown, accepts bounded click/wait actions plus window-targeted renderer reload/crash and synthetic open-url/second-instance delivery, and returns correlated window/WebContents/process/preload/session/navigation/shell/IPC evidence without retaining payload values. Results identify observed and unavailable event families, coverage status, action targets, and truncation. External shell, navigation, permission, download, popup, updater, and OS-integration effects are blocked and recorded. Use passive Electron tools for observation-only work.",
kind: "electron-provider",
inputSchema: electronActiveObservationInputSchema,
outputSchema: evidenceResultOf(electronActiveObservationResultSchema),
examples: [
{ title: "Exercise an owned Electron application", input: activeExample },
],
},
] as const satisfies readonly ToolContract[];

View File

@ -129,6 +129,16 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
}),
list_electron_targets: browserEvidence,
inspect_electron_page: browserEvidence,
capture_electron_scenario: effects({
mutatesTarget: true,
mutatesSession: true,
writesFilesystem: true,
launchesProcess: true,
accessesNetwork: true,
changesUiState: true,
mayDiscardData: true,
idempotent: false,
}),
list_javascript_runtime_targets: browserEvidence,
observe_javascript_runtime: browserEvidence,
analyze_javascript_application: evidence,

View File

@ -12,7 +12,8 @@ export type BrowserObservationOperation =
| "inspect_electron_page"
| "list_javascript_runtime_targets"
| "observe_javascript_runtime"
| "capture_browser_scenario";
| "capture_browser_scenario"
| "capture_electron_scenario";
/** Stable failure reasons shared by browser-family provider adapters. */
export type BrowserObservationFailureReason =
@ -24,5 +25,10 @@ export type BrowserObservationFailureReason =
| "protocol_error"
| "disconnected"
| "payload_limit"
| "window_not_found"
| "window_metadata_missing"
| "process_ownership_unavailable"
| "cleanup_failed"
| "cancelled"
| "timeout"
| "secret_unavailable";

View File

@ -4,7 +4,10 @@ import {
conformancePackageSchema,
type ConformancePackage,
} from "./conformancePackage.js";
import { trustGateResultSchema } from "./conformanceTrustGate.js";
import {
evaluatePackageTrustGates,
trustGateResultSchema,
} from "./conformanceTrustGate.js";
/** Result of a single scenario replay. */
export const scenarioReplayResultSchema = z.strictObject({
@ -31,6 +34,7 @@ export const packageReplayResultSchema = z.strictObject({
.strictObject({
scenario_id: z.string().min(1),
dimension: z.string().min(1),
evidence_id: z.string().min(1),
message: z.string(),
})
.nullable(),
@ -116,6 +120,7 @@ export async function replayConformancePackage(
? {
scenario_id: first_drift_gate.scenario_id,
dimension: first_drift_gate.first_divergence.dimension,
evidence_id: first_drift_gate.first_divergence.evidence_id,
message: first_drift_gate.first_divergence.message,
}
: null;
@ -133,6 +138,3 @@ export async function replayConformancePackage(
first_drift,
};
}
/** Import trust gate evaluator for replay. */
import { evaluatePackageTrustGates } from "./conformanceTrustGate.js";

View File

@ -47,12 +47,158 @@ const SEMANTIC_DIMENSIONS = new Set([
"stdout",
"stderr",
"filesystem",
"process",
"process_tree",
"shim_events",
"protocol_events",
"event_journal",
]);
type IncompleteStatus = "unknown" | "truncated";
type EvidenceView = {
readonly values: Readonly<Record<string, unknown>>;
readonly evidence_ids: readonly string[];
readonly status: IncompleteStatus | null;
readonly unknown_scopes: readonly string[];
};
type CompareDimensionOptions = {
readonly truncated?: boolean;
readonly evidence_ids?: readonly string[];
readonly expected_status?: IncompleteStatus | null;
readonly actual_status?: IncompleteStatus | null;
};
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const uniqueStrings = (values: readonly string[]): string[] => [
...new Set(values.filter((value) => value.length > 0)),
];
const stringValues = (value: unknown): readonly string[] => {
if (typeof value === "string") return value.length > 0 ? [value] : [];
if (!Array.isArray(value)) return [];
return value.filter(
(item): item is string => typeof item === "string" && item.length > 0,
);
};
const evidenceIds = (record: Readonly<Record<string, unknown>>): string[] =>
uniqueStrings([
...stringValues(record["evidence_id"]),
...stringValues(record["evidence_ids"]),
...stringValues(record["evidence_links"]),
]);
const incompleteStatus = (value: unknown): IncompleteStatus | null => {
if (!isRecord(value)) return null;
const record = value;
if (record["truncated"] === true) return "truncated";
const status = record["status"];
if (status === "truncated") return "truncated";
if (
status === "unknown" ||
status === "unavailable" ||
status === "partial" ||
status === "incomplete" ||
status === "unverifiable" ||
status === "failed" ||
status === "not_approved"
)
return "unknown";
for (const key of ["coverage", "completeness"]) {
const nested = record[key];
if (!isRecord(nested)) continue;
const nestedStatus = incompleteStatus(nested);
if (nestedStatus !== null) return nestedStatus;
}
return null;
};
const hasResidualUnknowns = (value: unknown): boolean =>
isRecord(value) &&
Array.isArray(value["residual_unknowns"]) &&
value["residual_unknowns"].length > 0;
const unknownScopes = (record: Readonly<Record<string, unknown>>): string[] => {
const residualUnknowns = record["residual_unknowns"];
if (!Array.isArray(residualUnknowns)) return [];
return residualUnknowns.flatMap((item) => {
if (!isRecord(item) || typeof item["scope"] !== "string") return [];
return [item["scope"]];
});
};
const evidenceView = (input: unknown): EvidenceView => {
if (!isRecord(input))
return {
values: {},
evidence_ids: [],
status: null,
unknown_scopes: [],
};
const normalized = input["normalized_result"];
const values = isRecord(normalized) ? normalized : input;
const normalizedStatus = isRecord(normalized)
? incompleteStatus(normalized)
: null;
return {
values,
evidence_ids: evidenceIds(input),
status: incompleteStatus(input) ?? normalizedStatus,
unknown_scopes: uniqueStrings([
...unknownScopes(input),
...(isRecord(normalized) ? unknownScopes(normalized) : []),
]),
};
};
const dimensionScope = (dimensionName: string): string | null => {
if (dimensionName === "exit_code" || dimensionName === "exit") return "exit";
if (
dimensionName === "process" ||
dimensionName === "process_tree" ||
dimensionName === "event_journal"
)
return "process";
if (dimensionName === "stdout" || dimensionName === "stderr")
return "terminal";
if (dimensionName === "filesystem") return "filesystem";
if (dimensionName === "shim_events") return "shim";
if (dimensionName === "protocol_events") return "protocol";
return null;
};
const statusForDimension = (
view: EvidenceView,
dimensionName: string,
): IncompleteStatus | null => {
const valueStatus = incompleteStatus(view.values[dimensionName]);
if (valueStatus !== null) return valueStatus;
if (hasResidualUnknowns(view.values[dimensionName])) return "unknown";
const scope = dimensionScope(dimensionName);
return scope !== null && view.unknown_scopes.includes(scope)
? "unknown"
: view.status;
};
const comparisonIncompleteStatus = (
expected: unknown,
actual: unknown,
options: CompareDimensionOptions,
): IncompleteStatus | null => {
const expectedStatus = options.expected_status ?? incompleteStatus(expected);
const actualStatus = options.actual_status ?? incompleteStatus(actual);
if (options.truncated) return "truncated";
if (expectedStatus === "truncated" || actualStatus === "truncated")
return "truncated";
if (expectedStatus === "unknown" || actualStatus === "unknown")
return "unknown";
return null;
};
/** Check if a dimension name is volatile (timing/normalization noise). */
export function isVolatileDimension(name: string): boolean {
return VOLATILE_DIMENSIONS.has(name);
@ -75,7 +221,7 @@ export function compareDimension(
dimensionName: string,
expected: unknown,
actual: unknown,
options: { truncated?: boolean } = {},
options: CompareDimensionOptions = {},
): DimensionResult {
// Volatile dimensions: always match regardless of values
if (isVolatileDimension(dimensionName)) {
@ -83,16 +229,22 @@ export function compareDimension(
name: dimensionName,
status: "match",
message: "volatile dimension ignored",
evidence_ids: [],
evidence_ids: uniqueStrings(options.evidence_ids ?? []),
};
}
if (options.truncated) {
const status = comparisonIncompleteStatus(expected, actual, options);
const resultEvidenceIds = uniqueStrings(options.evidence_ids ?? []);
if (status !== null) {
return {
name: dimensionName,
status: "truncated",
message: "evidence was truncated",
evidence_ids: [],
status,
message:
status === "truncated"
? "evidence was truncated"
: "evidence completeness is unknown",
evidence_ids: resultEvidenceIds,
};
}
@ -101,16 +253,16 @@ export function compareDimension(
name: dimensionName,
status: "unknown",
message: "both expected and actual are undefined",
evidence_ids: [],
evidence_ids: resultEvidenceIds,
};
}
if (expected === undefined || actual === undefined) {
return {
name: dimensionName,
status: "mismatch",
message: `expected ${expected === undefined ? "present" : "absent"}, actual ${actual === undefined ? "absent" : "present"}`,
evidence_ids: [],
status: "unknown",
message: "one side has no observed value",
evidence_ids: resultEvidenceIds,
};
}
@ -123,14 +275,14 @@ export function compareDimension(
name: dimensionName,
status: "match",
message: "",
evidence_ids: [],
evidence_ids: resultEvidenceIds,
};
}
return {
name: dimensionName,
status: "mismatch",
message: "semantic content differs",
evidence_ids: [],
evidence_ids: resultEvidenceIds,
};
}
@ -140,7 +292,7 @@ export function compareDimension(
name: dimensionName,
status: "match",
message: "",
evidence_ids: [],
evidence_ids: resultEvidenceIds,
};
}
@ -148,7 +300,7 @@ export function compareDimension(
name: dimensionName,
status: "mismatch",
message: `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
evidence_ids: [],
evidence_ids: resultEvidenceIds,
};
}
@ -156,9 +308,10 @@ export function compareDimension(
function sortKeys(value: unknown): unknown {
if (value === null || typeof value !== "object") return value;
if (Array.isArray(value)) return value.map(sortKeys);
if (!isRecord(value)) return value;
const sorted: Record<string, unknown> = {};
for (const key of Object.keys(value as Record<string, unknown>).sort()) {
sorted[key] = sortKeys((value as Record<string, unknown>)[key]);
for (const key of Object.keys(value).sort()) {
sorted[key] = sortKeys(value[key]);
}
return sorted;
}
@ -169,23 +322,28 @@ function sortKeys(value: unknown): unknown {
*/
export function evaluateTrustGate(
contract: VerifierContract,
expectedEvidence: Record<string, unknown>,
actualEvidence: Record<string, unknown>,
options: { truncated?: boolean } = {},
expectedEvidence: unknown,
actualEvidence: unknown,
options: { readonly truncated?: boolean } = {},
): TrustGateResult {
const dimensionResults: DimensionResult[] = [];
let firstDivergence: TrustGateResult["first_divergence"] = null;
const expectedView = evidenceView(expectedEvidence);
const actualView = evidenceView(actualEvidence);
for (const dim of contract.dimensions) {
if (!dim.required) continue;
const expectedValue = expectedEvidence[dim.name];
const actualValue = actualEvidence[dim.name];
const result = compareDimension(
dim.name,
expectedValue,
actualValue,
options,
);
const expectedValue = expectedView.values[dim.name];
const actualValue = actualView.values[dim.name];
const result = compareDimension(dim.name, expectedValue, actualValue, {
...options,
expected_status: statusForDimension(expectedView, dim.name),
actual_status: statusForDimension(actualView, dim.name),
evidence_ids: uniqueStrings([
...actualView.evidence_ids,
...expectedView.evidence_ids,
]),
});
dimensionResults.push(result);
if (result.status === "mismatch" && firstDivergence === null) {
@ -223,8 +381,8 @@ export function evaluateTrustGate(
*/
export function evaluatePackageTrustGates(
pkg: ConformancePackage,
actualEvidence: Record<string, Record<string, unknown>>,
options: { truncated?: boolean } = {},
actualEvidence: Readonly<Record<string, Readonly<Record<string, unknown>>>>,
options: { readonly truncated?: boolean } = {},
): TrustGateResult[] {
const results: TrustGateResult[] = [];
for (const contract of pkg.verifier_contracts) {
@ -236,7 +394,7 @@ export function evaluatePackageTrustGates(
const actual = actualEvidence[contract.scenario_id] ?? {};
const result = evaluateTrustGate(
contract,
expectedEvidence as Record<string, unknown>,
expectedEvidence,
actual,
options,
);

View File

@ -0,0 +1,296 @@
import { isAbsolute } from "node:path";
import { z } from "zod";
const absolutePathSchema = z
.string()
.min(1)
.max(16_384)
.refine(isAbsolute, "path must be absolute");
const windowIndexSchema = z.number().int().min(0).max(99);
const deepLinkUrlSchema = z
.string()
.min(1)
.max(4_096)
.refine((value) => {
try {
return new URL(value).protocol.length > 0;
} catch {
return false;
}
}, "deep-link url must be an absolute URL");
const actionSchema = z.discriminatedUnion("kind", [
z.strictObject({
step_id: z.string().min(1).max(128),
kind: z.literal("click"),
selector: z.string().min(1).max(512),
window_index: windowIndexSchema.default(0),
}),
z.strictObject({
step_id: z.string().min(1).max(128),
kind: z.literal("wait"),
duration_ms: z.number().int().min(0).max(10_000),
window_index: windowIndexSchema.optional(),
}),
z.strictObject({
step_id: z.string().min(1).max(128),
kind: z.literal("renderer-reload"),
window_index: windowIndexSchema.default(0),
}),
z.strictObject({
step_id: z.string().min(1).max(128),
kind: z.literal("renderer-crash"),
window_index: windowIndexSchema.default(0),
}),
z.strictObject({
step_id: z.string().min(1).max(128),
kind: z.literal("deep-link"),
delivery: z.enum(["open-url", "second-instance"]),
url: deepLinkUrlSchema,
}),
]);
const limitsSchema = z.strictObject({
max_duration_ms: z.number().int().min(1).max(120_000).default(60_000),
action_timeout_ms: z.number().int().min(1).max(30_000).default(5_000),
max_actions: z.number().int().min(1).max(100).default(20),
max_ipc_events: z.number().int().min(1).max(10_000).default(2_000),
max_runtime_events: z.number().int().min(1).max(20_000).default(5_000),
max_processes: z.number().int().min(1).max(100).default(32),
max_windows: z.number().int().min(1).max(100).default(32),
});
/** Input for one explicit, provider-owned Electron runtime experiment. */
export const electronActiveObservationInputSchema = z
.strictObject({
schema_version: z.literal(1),
executable_path: absolutePathSchema,
application_path: absolutePathSchema,
application_root: absolutePathSchema,
args: z.array(z.string().max(4_096)).max(32).default([]),
actions: z.array(actionSchema).max(100).default([]),
limits: limitsSchema.default({
max_duration_ms: 60_000,
action_timeout_ms: 5_000,
max_actions: 20,
max_ipc_events: 2_000,
max_runtime_events: 5_000,
max_processes: 32,
max_windows: 32,
}),
approved: z.literal(true),
})
.superRefine((input, context) => {
if (input.actions.length > input.limits.max_actions)
context.addIssue({
code: "custom",
path: ["actions"],
message: "actions exceed the configured max_actions limit",
});
});
export type ElectronActiveObservationInput = z.infer<
typeof electronActiveObservationInputSchema
>;
const ipcEventSchema = z.strictObject({
sequence: z.number().int().min(1),
correlation_id: z.string().max(256).nullable().default(null),
kind: z.enum([
"main-handler-invocation",
"main-event-invocation",
"utility-process-fork",
"utility-process-message",
"ipc-main-to-renderer",
"ipc-utility-to-main",
"ipc-renderer-send",
"ipc-renderer-invoke",
"ipc-renderer-post-message",
]),
event: z.string().max(128).nullable().optional(),
phase: z
.enum(["attempted", "completed", "blocked", "failed", "observed"])
.nullable()
.optional(),
channel: z.string().max(1_024).nullable(),
channel_truncated: z.boolean().default(false),
direction: z
.enum([
"renderer-to-main",
"main-to-renderer",
"main-to-utility",
"utility-to-main",
])
.nullable()
.optional(),
sender: z.string().max(256).nullable().optional(),
receiver: z.string().max(256).nullable().optional(),
frame: z.string().max(256).nullable().default(null),
target: z.string().max(256).nullable().optional(),
argument_shapes: z.array(z.string().max(64)).max(32),
argument_shapes_truncated: z.boolean().default(false),
result_shape: z.string().max(64).nullable(),
process_type: z.string().max(128).nullable(),
source: z.string().max(64).default("electron-active-hook"),
capture_method: z
.enum(["api-wrapper", "event-emitter", "process-hook"])
.default("api-wrapper"),
artifact_path: z.string().max(16_384).nullable().default(null),
artifact_sha256: z
.string()
.regex(/^[a-f0-9]{64}$/u)
.nullable()
.default(null),
error: z.boolean(),
});
const timelineEventSchema = z.strictObject({
sequence: z.number().int().min(1),
correlation_id: z.string().max(256).nullable().default(null),
kind: z.enum([
"main-handler-invocation",
"main-event-invocation",
"utility-process-fork",
"utility-process-message",
"ipc-main-to-renderer",
"ipc-utility-to-main",
"ipc-renderer-send",
"ipc-renderer-invoke",
"ipc-renderer-post-message",
"app-lifecycle",
"window-lifecycle",
"web-contents-lifecycle",
"navigation",
"shell-attempt",
"process-lifecycle",
"permission",
"popup-attempt",
"download",
"protocol",
"preload",
"native-addon",
"updater",
"error",
]),
event: z.string().max(128).nullable(),
phase: z.enum(["attempted", "completed", "blocked", "failed", "observed"]),
channel: z.string().max(1_024).nullable(),
channel_truncated: z.boolean(),
direction: z
.enum([
"renderer-to-main",
"main-to-renderer",
"main-to-utility",
"utility-to-main",
])
.nullable(),
sender: z.string().max(256).nullable(),
receiver: z.string().max(256).nullable(),
frame: z.string().max(256).nullable(),
target: z.string().max(256).nullable(),
argument_shapes: z.array(z.string().max(64)).max(32),
argument_shapes_truncated: z.boolean(),
result_shape: z.string().max(64).nullable(),
process_type: z.string().max(128).nullable(),
source: z.string().max(64),
capture_method: z.enum(["api-wrapper", "event-emitter", "process-hook"]),
artifact_path: z.string().max(16_384).nullable(),
artifact_sha256: z
.string()
.regex(/^[a-f0-9]{64}$/u)
.nullable(),
error: z.boolean(),
});
const processMetricSchema = z.strictObject({
pid: z.number().int().min(1),
type: z.string().min(1).max(128),
name: z.string().max(256).nullable(),
service_name: z.string().max(256).nullable(),
});
const windowResultSchema = z.strictObject({
window_id: z.string().max(256),
web_contents_id: z.string().max(256),
url: z.string().max(65_536),
title: z.string().max(16_384),
visible: z.boolean().nullable(),
destroyed: z.boolean(),
});
const coverageSchema = z.strictObject({
status: z.enum([
"complete",
"partial_attach",
"capture_truncated",
"hook_conflict",
"target_exited",
"cleanup_failed",
]),
observed_event_families: z.array(z.string().max(128)).max(32),
unavailable_event_families: z.array(z.string().max(128)).max(32),
observed_roles: z.array(z.string().max(128)).max(16),
pre_capture_activity: z.literal("unavailable"),
});
const actionResultSchema = z.strictObject({
step_id: z.string().min(1).max(128),
kind: z.enum([
"click",
"wait",
"renderer-reload",
"renderer-crash",
"deep-link",
]),
window_index: windowIndexSchema.nullable(),
target: z.string().max(256).nullable(),
status: z.enum(["completed", "failed", "cancelled"]),
elapsed_ms: z.number().int().min(0),
error: z.string().nullable(),
});
/** Bounded result of a provider-owned Electron runtime experiment. */
export const electronActiveObservationResultSchema = z.strictObject({
schema_version: z.literal(1),
application: z.strictObject({
executable_path: absolutePathSchema,
application_path: absolutePathSchema,
electron_version: z.string().min(1).max(256),
process_ownership: z.literal("provider-owned"),
cleanup: z.literal("terminated-owned-process"),
}),
actions: z.array(actionResultSchema).max(100),
windows: z.array(windowResultSchema).max(100),
windows_truncated: z.boolean(),
processes: z.strictObject({
items: z.array(processMetricSchema).max(100),
truncated: z.boolean(),
}),
ipc: z.strictObject({
events: z.array(ipcEventSchema).max(10_000),
observed: z.number().int().min(0),
retained: z.number().int().min(0),
truncated: z.boolean(),
}),
timeline: z
.strictObject({
events: z.array(timelineEventSchema).max(20_000),
observed: z.number().int().min(0),
retained: z.number().int().min(0),
truncated: z.boolean(),
})
.default({ events: [], observed: 0, retained: 0, truncated: false }),
coverage: coverageSchema.default({
status: "partial_attach",
observed_event_families: [],
unavailable_event_families: [],
observed_roles: [],
pre_capture_activity: "unavailable",
}),
limitations: z.array(z.string().max(4_096)).max(100),
});
export type ElectronActiveObservationResult = z.infer<
typeof electronActiveObservationResultSchema
>;

View File

@ -115,6 +115,7 @@ export const javascriptRuntimeLocationSchema = z.discriminatedUnion("kind", [
z.strictObject({
kind: z.literal("file"),
file_path: z.string().min(1).max(16_384),
authority: z.literal("scope-fallback").optional(),
}),
z.strictObject({
kind: z.literal("url"),

View File

@ -0,0 +1,138 @@
import { createHash } from "node:crypto";
import { isAbsolute, relative } from "node:path";
import canonicalize from "canonicalize";
import { z } from "zod";
import {
classifyBrowserCompleteness,
type BrowserCompleteness,
} from "./browserCompleteness.js";
import {
electronActiveObservationResultSchema,
type ElectronActiveObservationResult,
} from "./electronActiveObservation.js";
import type { Evidence } from "./evidence.js";
import type { ParsedRuntimeCapture } from "./javascriptRuntimeReconciliationParsing.js";
/** Parse active Electron Evidence as a bounded, target-only runtime capture. */
export const parseActiveElectronCapture = (
evidence: Evidence,
): ParsedRuntimeCapture => {
assertIdentity(evidence);
const result = electronActiveObservationResultSchema.parse(
evidence.normalized_result,
);
const parameters = z
.object({
approved: z.literal(true),
application_path: absolutePathSchema,
application_root: absolutePathSchema,
})
.passthrough()
.parse(evidence.parameters);
if (
result.application.application_path !== parameters.application_path ||
!isWithinRoot(
parameters.application_root,
result.application.application_path,
)
)
throw new TypeError(
"Active Electron Evidence application path disagrees with its approved root",
);
return {
kind: "electron-active",
evidence,
inspection: normalizeInspection(result),
captureSha256: digestCanonical(result),
scriptsCompleteWithinScope: false,
};
};
const absolutePathSchema = z.string().min(1).max(16_384).refine(isAbsolute);
const assertIdentity = (evidence: Evidence): void => {
if (
evidence.operation !== "capture_electron_scenario" ||
evidence.predicate_type !== "rea.electron-active-scenario/v1" ||
evidence.provider.id !== "rea-playwright-electron-active" ||
evidence.provider.name !==
"REA Playwright active Electron observation provider" ||
evidence.provider.version !== "1" ||
evidence.authority !== "controlled-replay" ||
evidence.confidence !== "observed"
)
throw new TypeError(
"Evidence does not match the supported capture_electron_scenario contract",
);
};
const normalizeInspection = (
result: ElectronActiveObservationResult,
): {
readonly target: {
readonly target_id: string;
readonly type: string;
readonly title: string;
readonly attached: boolean;
readonly file_path: string;
};
readonly frames: readonly [];
readonly scripts: { readonly items: readonly [] };
readonly workers: readonly [];
readonly completeness: BrowserCompleteness;
} => ({
target: {
target_id: `electron-active:${digestCanonical(result.application.application_path).slice(0, 32)}`,
type: "electron-application",
title: result.application.application_path,
attached: true,
file_path: result.application.application_path,
},
frames: [],
scripts: { items: [] },
workers: [],
completeness: classifyBrowserCompleteness({
policyFilteredSections: new Set(),
attachLimitedSections: new Set(),
truncatedSections: new Set(
result.windows_truncated ||
result.processes.truncated ||
result.ipc.truncated ||
result.timeline.truncated
? ["timeline"]
: [],
),
unavailableSections: new Set([
"frames",
"scripts",
"script_sources",
"workers",
]),
excluded: [],
droppedEvents: {
scripts: 0,
network_requests: 0,
console_events: 0,
websocket_connections: 0,
websocket_frames: 0,
webmcp_tools: 0,
timeline_events: result.timeline.truncated ? result.timeline.observed : 0,
},
}),
});
const digestCanonical = (value: unknown): string => {
const encoded = canonicalize(value);
if (encoded === undefined)
throw new TypeError("Runtime reconciliation could not canonicalize input");
return createHash("sha256").update(encoded).digest("hex");
};
const isWithinRoot = (root: string, path: string): boolean => {
const remainder = relative(root, path);
return (
remainder === "" || (!remainder.startsWith("..") && !isAbsolute(remainder))
);
};

View File

@ -34,6 +34,7 @@ import {
compareCodePoints,
type JavaScriptApplicationGraph,
} from "./javascriptApplicationGraph.js";
import { parseActiveElectronCapture } from "./javascriptRuntimeReconciliationActive.js";
import type { ReconcileJavaScriptRuntimeInput } from "./javascriptRuntimeReconciliationSchemas.js";
type StaticLayerInput =
@ -101,6 +102,22 @@ export type ParsedRuntimeCapture =
| (RuntimeCaptureBase & {
readonly kind: "v8-inspector";
readonly inspection: NormalizedV8Inspection;
})
| (RuntimeCaptureBase & {
readonly kind: "electron-active";
readonly inspection: {
readonly target: {
readonly target_id: string;
readonly type: string;
readonly title: string;
readonly attached: boolean;
readonly file_path: string;
};
readonly frames: readonly [];
readonly scripts: { readonly items: readonly [] };
readonly workers: readonly [];
readonly completeness: BrowserCompleteness;
};
});
/** Parse and semantically bind every static analysis Evidence layer. */
@ -255,8 +272,11 @@ const parseRuntimeCapture = (input: Evidence): ParsedRuntimeCapture => {
scriptsCompleteWithinScope: false,
};
}
if (evidence.operation === "capture_electron_scenario") {
return parseActiveElectronCapture(evidence);
}
throw new TypeError(
"Runtime reconciliation requires inspect_web_page, inspect_electron_page, or observe_javascript_runtime Evidence",
"Runtime reconciliation requires inspect_web_page, inspect_electron_page, observe_javascript_runtime, or capture_electron_scenario Evidence",
);
};

View File

@ -170,7 +170,7 @@ const layerSummarySchema = z.strictObject({
const captureSummarySchema = z.strictObject({
evidence_id: evidenceIdSchema,
capture_sha256: digestSchema,
kind: z.enum(["browser", "electron", "v8-inspector"]),
kind: z.enum(["browser", "electron", "v8-inspector", "electron-active"]),
target_node_id: nodeIdSchema,
target_key: boundedTextSchema,
target_location: boundedTextSchema,

View File

@ -8,6 +8,7 @@ export type PermissionCapability =
| "browser_observe"
| "browser_automate"
| "electron_observe"
| "electron_automate"
| "v8_inspector_observe"
| "evidence_read"
| "evidence_write"

File diff suppressed because one or more lines are too long

View File

@ -15,6 +15,7 @@ import {
import type { AppConfig } from "../config.js";
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
import type { BinaryTarget } from "../domain/binaryTarget.js";
import { jsonObjectSchema } from "../domain/jsonValue.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
@ -61,6 +62,11 @@ import {
windowsP0Limitations,
limitationsFor,
} from "./GhidraProviderCapabilities.js";
import {
WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON,
hasWindowsNativeAuthority,
windowsNativeCapabilities,
} from "../process/WindowsAuthority.js";
export { GHIDRA_PROVIDER_IDENTITY, GHIDRA_PROVIDER_TOOL_CONTRACTS };
@ -98,6 +104,22 @@ export class GhidraProvider implements AnalysisProviderCandidate {
inspectAvailability(): ProviderAvailability {
const installation = this.#inspectInstallation();
const diagnostics = ghidraInstallationDiagnostics(installation);
if (
installation.available &&
installation.platform === "win32" &&
!hasWindowsNativeAuthority(installation.platform)
)
return {
status: "unavailable",
code: "unsupported_host",
reason: WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON,
diagnostics: {
...diagnostics,
windows_security: jsonObjectSchema.parse(
windowsNativeCapabilities(installation.platform),
),
},
};
return installation.available
? {
status: "available",
@ -362,6 +384,17 @@ const ghidraClientPrerequisites = (
diagnostics: ghidraInstallationDiagnostics(installation),
}),
);
if (
installation.platform === "win32" &&
!hasWindowsNativeAuthority(installation.platform)
)
return err(
new AnalysisCapabilityUnavailableError(
"ghidra",
"health",
WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON,
),
);
if (profile === undefined || profile.provider.id !== "ghidra")
return err(new ProviderAdapterError("ghidra", "health"));
return ok({

View File

@ -2,6 +2,7 @@ import type {
CapabilityDescriptor,
ProviderIdentity,
} from "../application/AnalysisProvider.js";
import { WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON } from "../process/WindowsAuthority.js";
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
import {
GHIDRA_DECOMPILE_REQUEST_TIMEOUT_MS,
@ -67,7 +68,7 @@ export const healthLimitations = Object.freeze([
export const windowsP0Limitations = Object.freeze([
"Windows Ghidra P0 accepts approved native x86-64 PE applications only; DLL, managed, hostile, sensitive, and mutable-path targets are unsupported.",
"The Windows bridge uses authenticated IPv4 loopback because Node path-based IPC does not expose Java AF_UNIX sockets; the endpoint file contains no bearer token.",
"Windows P0 process-tree cleanup uses bounded taskkill termination and does not claim Job Object ownership, private DACL enforcement, or reparse-point-safe path authority.",
"Windows P0 is not admitted until a native authority proves Job Object ownership, private DACL enforcement, and reparse-safe path admission; bounded taskkill cleanup and chmod(0700) do not prove those controls.",
]);
/** Limitation text for one admitted operation, including the common base. */
@ -207,6 +208,9 @@ export const WINDOWS_P0_CAPABILITIES: readonly CapabilityDescriptor[] =
CAPABILITIES.map((capability) =>
Object.freeze({
...capability,
available: false,
reason: WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON,
availabilityCode: "unsupported_host" as const,
limitations: Object.freeze([
...capability.limitations,
...windowsP0Limitations,

View File

@ -85,17 +85,20 @@ export const run = async (
{ CdpBrowserProvider },
{ PlaywrightBrowserScenarioProvider },
{ CdpElectronProvider },
{ PlaywrightElectronActiveProvider },
{ V8InspectorProvider },
] = await Promise.all([
import("./browser/CdpBrowserProvider.js"),
import("./browser/PlaywrightBrowserScenarioProvider.js"),
import("./browser/CdpElectronProvider.js"),
import("./browser/PlaywrightElectronActiveProvider.js"),
import("./browser/V8InspectorProvider.js"),
]);
return {
browserObservation: new CdpBrowserProvider(),
browserScenarioCapture: new PlaywrightBrowserScenarioProvider(),
electronObservation: new CdpElectronProvider(),
electronActiveObservation: new PlaywrightElectronActiveProvider(),
javascriptRuntimeObservation: new V8InspectorProvider(),
};
},

View File

@ -104,6 +104,10 @@ const runtimeAvailability = (state: RuntimeState) => ({
state.currentConfig.electronObservationEnabled &&
state.currentConfig.electronCdpEndpoints.length > 0 &&
state.currentConfig.electronFileRoots.length > 0,
electronAutomationEnabled:
state.currentConfig.electronAutomationPolicy.enabled &&
state.currentConfig.electronAutomationPolicy.executableRoots.length > 0 &&
state.currentConfig.electronAutomationPolicy.applicationRoots.length > 0,
v8InspectorObservationEnabled:
state.currentConfig.v8InspectorObservationEnabled &&
state.currentConfig.v8InspectorEndpoints.length > 0 &&

View File

@ -2,10 +2,44 @@ import { chmod, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
windowsNativeCapabilities,
type WindowsNativeCapability,
} from "./WindowsAuthority.js";
/** Filesystem coordinates for one provider-owned private runtime directory. */
export interface PrivateRuntimeRootOptions {
readonly parent?: string;
readonly prefix?: string;
/** Platform override used by deterministic boundary tests. */
readonly platform?: NodeJS.Platform;
}
/** Provider-neutral security status for one runtime-root implementation. */
export type PrivateRuntimeRootCapability =
| WindowsNativeCapability
| {
readonly available: true;
readonly reason: null;
readonly proof: "posix-mode-0700";
};
/** Report whether this process can establish a private runtime root. */
export const privateRuntimeRootCapability = (
platform: NodeJS.Platform = process.platform,
): PrivateRuntimeRootCapability =>
platform === "win32"
? windowsNativeCapabilities(platform).private_runtime_dacl
: { available: true, reason: null, proof: "posix-mode-0700" };
/** Expected failure when a runtime root's privacy boundary is unavailable. */
export class PrivateRuntimeRootUnavailableError extends Error {
readonly code = "private-runtime-root-authority-unavailable";
constructor(message: string) {
super(message);
this.name = "PrivateRuntimeRootUnavailableError";
}
}
/**
@ -24,6 +58,10 @@ export class PrivateRuntimeRoot {
static async create(
options: PrivateRuntimeRootOptions = {},
): Promise<PrivateRuntimeRoot> {
const platform = options.platform ?? process.platform;
const capability = privateRuntimeRootCapability(platform);
if (!capability.available)
throw new PrivateRuntimeRootUnavailableError(capability.reason);
const path = await mkdtemp(
join(options.parent ?? tmpdir(), options.prefix ?? "rea-provider-"),
);

View File

@ -15,6 +15,8 @@ export interface OwnedProcessGroup {
readonly expectedCommand?: string;
/** Expected launcher parent, checked only while the leader exists. */
readonly expectedParentPid?: number;
/** Scan all live processes for this run token during cleanup. */
readonly sweepTokenOwnedProcesses?: boolean;
}
/** One entry from an operating-system process-table snapshot. */
@ -96,7 +98,8 @@ export type ProcessGroupObservation =
* Terminate one Windows process tree through the platform utility.
*
* This is a bounded P0 cleanup mechanism, not Job Object ownership proof. The
* caller-visible Windows limitations must preserve that distinction.
* caller-visible Windows capability report remains unavailable until a native
* authority verifies Job Object creation, membership, and cleanup semantics.
*/
export const cleanupWindowsProcessTree = async (
rootPid: number,
@ -250,6 +253,24 @@ export const cleanupOwnedProcessGroup = async (
return { cleaned: true, signaled };
};
/** Verify that no live process still carries an owned capture run token. */
export const verifyNoTokenOwnedProcesses = async (
runId: string,
host: ProcessOwnershipHost = systemHost,
): Promise<ProcessCleanupResult> => {
let processes: readonly ProcessTableEntry[];
try {
processes = liveProcesses(await host.listProcesses());
} catch {
return { cleaned: false, reason: "process table could not be inspected" };
}
const scan = await scanTokenOwnedProcesses(processes, runId, host);
if (scan.failures.length > 0) return cleanupValidationFailure(scan.failures);
return scan.owned.length === 0
? { cleaned: true, signaled: false }
: { cleaned: false, reason: "token-owned process remained after cleanup" };
};
interface OwnedCleanupPlan {
readonly signalOrder: readonly number[];
}
@ -259,11 +280,24 @@ const createOwnedCleanupPlan = async (
processes: readonly ProcessTableEntry[],
host: ProcessOwnershipHost,
): Promise<OwnedCleanupPlan | ProcessCleanupResult> => {
const tokenOwned =
ownership.sweepTokenOwnedProcesses === true
? await scanTokenOwnedProcesses(processes, ownership.runId, host)
: { owned: [], failures: [] };
if (tokenOwned.failures.length > 0)
return cleanupValidationFailure(tokenOwned.failures);
const tokenOwnedGroupIds = new Set(
tokenOwned.owned.map(({ processGroupId }) => processGroupId),
);
const launcher = processes.find(({ pid }) => pid === ownership.leaderPid);
const rootMembers = processes.filter(
({ processGroupId }) => processGroupId === ownership.processGroupId,
);
if (launcher === undefined && rootMembers.length === 0)
if (
launcher === undefined &&
rootMembers.length === 0 &&
tokenOwned.owned.length === 0
)
return { cleaned: true, signaled: false };
let descendants: readonly ProcessTableEntry[] = [];
if (launcher !== undefined) {
@ -276,13 +310,18 @@ const createOwnedCleanupPlan = async (
const processGroupIds = new Set<number>([ownership.processGroupId]);
for (const descendant of descendants)
processGroupIds.add(descendant.processGroupId);
for (const process of tokenOwned.owned)
processGroupIds.add(process.processGroupId);
for (const processGroupId of processGroupIds) {
if (processGroupId === ownership.processGroupId) continue;
const groupLeader = processes.find(
({ pid, processGroupId: observedGroupId }) =>
pid === processGroupId && observedGroupId === processGroupId,
);
if (groupLeader === undefined || !descendantPids.has(groupLeader.pid))
if (
(groupLeader === undefined || !descendantPids.has(groupLeader.pid)) &&
!tokenOwnedGroupIds.has(processGroupId)
)
return {
cleaned: false,
reason:
@ -360,7 +399,8 @@ const revalidateOwnedProcessGroup = async (
if (
processGroupId !== ownership.processGroupId &&
members.length > 0 &&
!members.some(({ pid }) => pid === processGroupId)
!members.some(({ pid }) => pid === processGroupId) &&
ownership.sweepTokenOwnedProcesses !== true
)
return {
cleaned: false,
@ -401,6 +441,33 @@ const processOwnershipFailures = async (
return failures;
};
interface TokenOwnedProcessScan {
readonly owned: readonly ProcessTableEntry[];
readonly failures: readonly ProcessOwnershipValidationFailure[];
}
const scanTokenOwnedProcesses = async (
processes: readonly ProcessTableEntry[],
runId: string,
host: ProcessOwnershipHost,
): Promise<TokenOwnedProcessScan> => {
const owned: ProcessTableEntry[] = [];
const failures: ProcessOwnershipValidationFailure[] = [];
for (const process of processes) {
try {
if ((await host.environment(process.pid)).REA_PROCESS_RUN_ID === runId)
owned.push(process);
} catch {
try {
const live = liveProcesses(await host.listProcesses());
if (!live.some(({ pid }) => pid === process.pid)) continue;
} catch {}
failures.push({ pid: process.pid, reason: "environment-unreadable" });
}
}
return { owned, failures };
};
/** Observe one group without signaling it, failing closed on identity doubt. */
export const observeOwnedProcessGroup = async (
ownership: OwnedProcessGroup,

View File

@ -0,0 +1,65 @@
/**
* Provider-neutral security controls that require a verified Windows native
* authority. Runtime probes and POSIX permissions do not satisfy these
* controls.
*/
export type WindowsNativeCapability =
| {
readonly available: true;
readonly reason: null;
readonly proof: "native-authority";
}
| {
readonly available: false;
readonly reason: string;
readonly proof: "not-proven" | "not-applicable";
};
/** Security controls required before REA may claim Windows isolation. */
export interface WindowsNativeCapabilitySet {
readonly job_object_process_ownership: WindowsNativeCapability;
readonly private_runtime_dacl: WindowsNativeCapability;
readonly reparse_safe_path_admission: WindowsNativeCapability;
}
/** Stable explanation used whenever the native Windows authority is absent. */
export const WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON =
"Verified Windows native authority is unavailable: Job Object process ownership, private runtime DACL enforcement, and reparse-safe path admission are not implemented; chmod(0700) is insufficient for Windows private DACL proof, and taskkill is not a Job Object security proof";
/**
* Describe native Windows controls without inferring them from taskkill,
* chmod, symlink creation, or a numeric POSIX open flag.
*/
export const windowsNativeCapabilities = (
platform: NodeJS.Platform,
): WindowsNativeCapabilitySet => {
if (platform !== "win32") {
const notApplicable = {
available: false,
reason: "Windows native authority is not applicable on this host",
proof: "not-applicable",
} as const;
return {
job_object_process_ownership: notApplicable,
private_runtime_dacl: notApplicable,
reparse_safe_path_admission: notApplicable,
};
}
const notProven = {
available: false,
reason: WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON,
proof: "not-proven",
} as const;
return {
job_object_process_ownership: notProven,
private_runtime_dacl: notProven,
reparse_safe_path_admission: notProven,
};
};
/** Whether every native control needed for Windows isolation is proven. */
export const hasWindowsNativeAuthority = (platform: NodeJS.Platform): boolean =>
Object.values(windowsNativeCapabilities(platform)).every(
({ available }) => available,
);

View File

@ -19,6 +19,7 @@ import type { PermissionAuthority } from "../application/PermissionAuthority.js"
import type { BrowserObservationPort } from "../application/BrowserObservationPort.js";
import type { BrowserScenarioCapturePort } from "../application/BrowserScenarioCapturePort.js";
import type { ElectronObservationPort } from "../application/ElectronObservationPort.js";
import type { ElectronActiveObservationPort } from "../application/ElectronActiveObservationPort.js";
import type { JavaScriptRuntimeObservationPort } from "../application/JavaScriptRuntimeObservationPort.js";
import type { SessionAvailability } from "./sessionAvailabilityPolicy.js";
import { sessionAvailabilityPolicy } from "./sessionAvailabilityPolicy.js";
@ -29,7 +30,7 @@ import {
import { installDynamicToolAvailability } from "./DynamicToolAvailability.js";
const TARGET_FREE_INSTRUCTIONS =
"ASAR/JavaScript -> analyze_javascript_application; archive/package -> open_binary(path), then inspect_artifact/inventory_artifact (active target); managed PE/CLI -> inspect_managed_artifact; passive browser/Electron -> list_browser_targets/list_electron_targets; approved browser interaction -> capture_browser_scenario; Node/Electron Inspector -> list_javascript_runtime_targets; native binary/database -> open_binary, then binary_overview. Start with binary_session; call only tools in tools/list. Hidden routes: binary_session detail=capabilities. Use summaries, cite Evidence IDs. Never repeat identical analysis or read full Evidence.";
"ASAR/JavaScript -> analyze_javascript_application; archive/package -> open_binary(path), then inspect_artifact/inventory_artifact (active target); managed PE/CLI -> inspect_managed_artifact; browser/Electron -> list_browser_targets/list_electron_targets; approved -> capture_browser_scenario/capture_electron_scenario; Node/Electron Inspector -> list_javascript_runtime_targets; native binary/database -> open_binary, then binary_overview. Start with binary_session; use tools/list; capabilities via binary_session. Use summaries, cite Evidence IDs; Never repeat identical analysis or read full Evidence.";
const ACTIVE_TARGET_INSTRUCTIONS =
"REA analyzes the active reverse-engineering target. Start native analysis with binary_overview, then narrow with analyze_function, literal search, callers, callees, and xrefs. Prefer summary views, never repeat an identical call, and read full Evidence only when the task requires it.";
@ -55,6 +56,7 @@ export interface CreateServerOptions {
readonly browserObservation?: BrowserObservationPort;
readonly browserScenarioCapture?: BrowserScenarioCapturePort;
readonly electronObservation?: ElectronObservationPort;
readonly electronActiveObservation?: ElectronActiveObservationPort;
readonly javascriptRuntimeObservation?: JavaScriptRuntimeObservationPort;
readonly artifactIntegrityContinueEnabled?: () => boolean;
readonly javascriptReplayPolicy?: JavaScriptReplayPolicy;
@ -66,6 +68,7 @@ export interface CreateServerOptions {
readonly browserObservation: BrowserObservationPort;
readonly browserScenarioCapture: BrowserScenarioCapturePort;
readonly electronObservation: ElectronObservationPort;
readonly electronActiveObservation: ElectronActiveObservationPort;
readonly javascriptRuntimeObservation: JavaScriptRuntimeObservationPort;
}>;
}
@ -84,6 +87,9 @@ const installSessionToolAvailability = (
browserObservationEnabled: options.browserObservation !== undefined,
browserScenarioEnabled: options.browserScenarioCapture !== undefined,
electronObservationEnabled: options.electronObservation !== undefined,
electronAutomationEnabled:
options.electronActiveObservation !== undefined &&
options.permissionAuthority !== undefined,
v8InspectorObservationEnabled:
options.javascriptRuntimeObservation !== undefined,
javascriptReplayEnabled: options.javascriptReplayPolicy?.enabled ?? false,

View File

@ -158,6 +158,7 @@ const hydrateObservationFamily = async (
registerElectronTools(context.server, {
...common,
electron: context.options.electronObservation,
electronActive: context.options.electronActiveObservation,
});
return;
}

View File

@ -3,6 +3,8 @@ import type { z } from "zod";
import type { BinarySessionPort } from "../application/BinarySession.js";
import type { ElectronObservationPort } from "../application/ElectronObservationPort.js";
import type { ElectronActiveObservationPort } from "../application/ElectronActiveObservationPort.js";
import { captureElectronScenario } from "../application/ElectronActiveObservationService.js";
import { analyzeJavaScriptApplicationValidated } from "../application/JavaScriptApplicationService.js";
import { reconcileJavaScriptRuntimeEvidenceValidated } from "../application/JavaScriptRuntimeReconciliationService.js";
import {
@ -18,6 +20,7 @@ import {
inspectElectronPageInputSchema,
listElectronTargetsInputSchema,
} from "../domain/electronObservation.js";
import { electronActiveObservationInputSchema } from "../domain/electronActiveObservation.js";
import { reconcileJavaScriptRuntimeInputSchema } from "../domain/javascriptRuntimeReconciliationSchemas.js";
import type { Logger } from "../logger.js";
import { logToolExecution } from "./toolLogging.js";
@ -36,6 +39,7 @@ import { summarizeJavaScriptApplicationEvidence } from "./javascriptApplicationR
interface ElectronToolRegistration {
readonly logger: Logger;
readonly electron: ElectronObservationPort | undefined;
readonly electronActive: ElectronActiveObservationPort | undefined;
readonly permissionAuthority: PermissionAuthority | undefined;
readonly recordEvidence: BinarySessionPort["recordEvidence"] | undefined;
}
@ -66,8 +70,13 @@ export const registerElectronTools = (
server: McpServer,
options: ElectronToolRegistration,
): void => {
const [listContract, inspectContract, analyzeContract, reconcileContract] =
ELECTRON_TOOL_CONTRACTS;
const [
listContract,
inspectContract,
analyzeContract,
reconcileContract,
activeContract,
] = ELECTRON_TOOL_CONTRACTS;
registerElectronTool(server, options, {
contract: listContract,
@ -118,6 +127,17 @@ export const registerElectronTools = (
execute: (parsed, _context) =>
Promise.resolve(reconcileJavaScriptRuntimeEvidenceValidated(parsed)),
});
registerElectronTool(server, options, {
contract: activeContract,
schema: electronActiveObservationInputSchema,
execute: (parsed, { signal, progress }) =>
captureElectronScenario(
options.electronActive,
options.permissionAuthority,
parsed,
{ signal, progress },
),
});
};
const registerElectronTool = <Schema extends z.ZodType>(

View File

@ -13,6 +13,7 @@ export interface SessionAvailabilityDefaults {
| "browserObservationEnabled"
| "browserScenarioEnabled"
| "electronObservationEnabled"
| "electronAutomationEnabled"
| "v8InspectorObservationEnabled"
| "javascriptReplayEnabled"
| "managedRuntimeEnabled"
@ -35,6 +36,8 @@ export const sessionAvailabilityPolicy = (
defaults.optionalFeatures?.browserScenarioEnabled ?? false,
electronObservationEnabled:
defaults.optionalFeatures?.electronObservationEnabled ?? false,
electronAutomationEnabled:
defaults.optionalFeatures?.electronAutomationEnabled ?? false,
v8InspectorObservationEnabled:
defaults.optionalFeatures?.v8InspectorObservationEnabled ?? false,
javascriptReplayEnabled:

View File

@ -80,6 +80,18 @@ describe("package installation workflows", () => {
expect(release).not.toContain('npm run verify:published -- "${version}"');
expect(canary).toContain('mkdtemp(join(tmpdir(), "rea-published-canary-")');
expect(canary).toContain("cwd: canaryRoot");
expect(release).toContain("publish-mcp:");
expect(release).toContain("needs: [release-please, publish]");
expect(release).toContain("MCP_PUBLISHER_VERSION: v1.8.0");
expect(release).toContain("mcp-publisher validate");
expect(release).toContain("mcp-publisher login github-oidc");
expect(release).toContain("mcp-publisher publish");
expect(release).toContain("id-token: write");
expect(release).toContain("contents: read");
expect(release).not.toContain("MCP_GITHUB_TOKEN");
expect(release.indexOf("publish-mcp:")).toBeGreaterThan(
release.indexOf("npm publish --access public"),
);
});
it("uses direct Node ownership on macOS and Windows", () => {

View File

@ -0,0 +1,135 @@
import { expect, it } from "vitest";
import { electronActiveObservationInputSchema } from "../../../src/domain/electronActiveObservation.js";
import { runElectronActions } from "../../../src/browser/PlaywrightElectronActiveActions.js";
it("parses bounded window, renderer, and deep-link actions for agents", () => {
const input = electronActiveObservationInputSchema.parse({
schema_version: 1,
executable_path: "/opt/electron",
application_path: "/opt/app/main.js",
application_root: "/opt/app",
actions: [
{ step_id: "first-window", kind: "click", selector: "#run" },
{ step_id: "second-window", kind: "renderer-reload", window_index: 1 },
{ step_id: "restart", kind: "renderer-crash", window_index: 1 },
{
step_id: "deep-link",
kind: "deep-link",
delivery: "second-instance",
url: "rea-fixture://open/item",
},
],
approved: true,
});
expect(input.actions).toEqual([
{
step_id: "first-window",
kind: "click",
selector: "#run",
window_index: 0,
},
{ step_id: "second-window", kind: "renderer-reload", window_index: 1 },
{ step_id: "restart", kind: "renderer-crash", window_index: 1 },
{
step_id: "deep-link",
kind: "deep-link",
delivery: "second-instance",
url: "rea-fixture://open/item",
},
]);
});
it("rejects non-absolute deep-link values", () => {
const result = electronActiveObservationInputSchema.safeParse({
schema_version: 1,
executable_path: "/opt/electron",
application_path: "/opt/app/main.js",
application_root: "/opt/app",
actions: [
{
step_id: "bad-link",
kind: "deep-link",
delivery: "open-url",
url: "not a URL",
},
],
approved: true,
});
expect(result.success).toBe(false);
});
it("redacts action inputs from Playwright failures", async () => {
const selector = "#secret-selector";
const input = electronActiveObservationInputSchema.parse({
schema_version: 1,
executable_path: "/opt/electron",
application_path: "/opt/app/main.js",
application_root: "/opt/app",
actions: [{ step_id: "click-step", kind: "click", selector }],
approved: true,
});
const page = {
locator: () => ({
click: async () => {
throw new Error(`locator ${selector} failed with token=raw-secret`);
},
}),
};
const application = {
windows: () => [page],
};
const result = await runElectronActions(
application as never,
input,
{},
Date.now() + 1_000,
);
expect(result[0]).toMatchObject({
status: "failed",
error: "locator <redacted-input> failed with token=[REDACTED]",
});
expect(result[0]?.error).not.toContain(selector);
expect(result[0]?.error).not.toContain("raw-secret");
});
it("runs an untargeted deep-link without requiring a BrowserWindow", async () => {
const input = electronActiveObservationInputSchema.parse({
schema_version: 1,
executable_path: "/opt/electron",
application_path: "/opt/app/main.js",
application_root: "/opt/app",
actions: [
{
step_id: "open-deep-link",
kind: "deep-link",
delivery: "second-instance",
url: "rea-fixture://open/item",
},
],
approved: true,
});
const application = {
windows: () => [],
evaluate: async () => true,
};
const result = await runElectronActions(
application as never,
input,
{},
Date.now() + 1_000,
);
expect(result).toMatchObject([
{
step_id: "open-deep-link",
status: "completed",
error: null,
},
]);
});

View File

@ -0,0 +1,242 @@
import { rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { spawn } from "node:child_process";
import { afterEach, expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const temporary: string[] = [];
afterEach(async () => {
await Promise.all(
temporary
.splice(0)
.map(async (path) => rm(path, { force: true, recursive: true })),
);
});
it("keeps active hook lifecycle and IPC evidence bounded at the process boundary", async () => {
const root = await createTestTempDirectory("rea-electron-hook-");
temporary.push(root);
await writeFile(
join(root, "electron.js"),
`const { EventEmitter } = require("node:events");
const app = new EventEmitter();
app.relaunch = () => undefined;
app.setAsDefaultProtocolClient = () => true;
app.setLoginItemSettings = () => undefined;
const ipcMain = new EventEmitter();
ipcMain.handlers = new Map();
ipcMain.handle = (channel, listener) => ipcMain.handlers.set(channel, listener);
const ipcRenderer = {
send: () => undefined,
sendSync: () => "ok",
invoke: () => Promise.resolve("ok"),
postMessage: () => undefined,
};
class WebContents extends EventEmitter {
constructor() {
super();
this.id = 12;
}
loadFile() { return Promise.resolve(); }
loadURL() { return Promise.resolve(); }
loadDataURL() { return Promise.resolve(); }
send() { return undefined; }
postMessage() { return undefined; }
setWindowOpenHandler(handler) { this.windowOpenHandler = handler; return undefined; }
}
class BrowserWindow extends EventEmitter {
constructor(options) {
super();
this.id = 3;
this.webContents = new WebContents();
this.options = options;
}
}
const utilityProcess = {
fork: () => {
const child = new EventEmitter();
child.pid = 44;
child.postMessage = () => undefined;
return child;
},
};
const shell = { openExternal: () => Promise.resolve(), openPath: () => Promise.resolve() };
const createSession = () => {
const value = new EventEmitter();
value.setPermissionRequestHandler = () => undefined;
value.setPermissionCheckHandler = () => undefined;
value.setCertificateVerifyProc = () => undefined;
return value;
};
const session = createSession();
const customSession = createSession();
const contextBridge = {
exposeInMainWorld: () => undefined,
exposeInIsolatedWorld: () => undefined,
};
module.exports = {
app,
BrowserWindow,
contextBridge,
ipcMain,
ipcRenderer,
session: {
defaultSession: session,
fromPartition: () => customSession,
},
shell,
utilityProcess,
};
`,
);
const script = join(root, "exercise.cjs");
await writeFile(
script,
`(async () => {
const electron = require("electron");
const window = new electron.BrowserWindow({ webPreferences: { preload: "/approved/preload.js" } });
const channel = "x".repeat(2048);
electron.ipcMain.handle(channel, async () => ({ ok: true }));
await electron.ipcMain.handlers.get(channel)({ sender: { id: 9 }, senderFrame: { routingId: 7 } }, ...Array.from({ length: 40 }, () => ({ value: true })));
window.webContents.send("main-to-renderer", { value: true });
window.webContents.emit("render-process-gone", { reason: "crashed" });
const utility = electron.utilityProcess.fork("utility.js");
utility.emit("message", { data: { value: true } });
utility.postMessage({ value: true });
electron.session.defaultSession.emit("will-download", window.webContents, { cancel() {} });
electron.session.fromPartition("persist:custom").emit("will-download", window.webContents, { cancel() {} });
const defaultPopupDecision = window.webContents.windowOpenHandler({ url: "https://default-popup.invalid" });
window.webContents.setWindowOpenHandler(() => ({ action: "allow" }));
const popupDecision = window.webContents.windowOpenHandler({ url: "https://popup.invalid" });
electron.shell.openExternal("https://example.invalid").catch(() => undefined);
electron.app.relaunch();
electron.contextBridge.exposeInMainWorld("api", { value: true });
window.webContents.loadURL("https://example.invalid").catch(() => undefined);
try { process.dlopen({}, "/missing/native.node"); } catch {}
process.stdout.write(JSON.stringify({ snapshot: globalThis.__reaElectronActiveSnapshot(), default_popup_decision: defaultPopupDecision, popup_decision: popupDecision }) + "\\n");
})();
`,
);
const hook = join(process.cwd(), "scripts/electron-active-hook.cjs");
const output = await runNode(hook, script, root, root);
const result = JSON.parse(output.trim().split("\n").at(-1) ?? "null") as {
readonly default_popup_decision: { readonly action: string };
readonly popup_decision: { readonly action: string };
readonly snapshot: {
readonly hook_error: boolean;
readonly events: readonly Record<string, unknown>[];
};
};
const { snapshot } = result;
expect(snapshot.hook_error).toBe(false);
expect(snapshot.events).toEqual(
expect.arrayContaining([
expect.objectContaining({ kind: "window-lifecycle", event: "created" }),
expect.objectContaining({ kind: "preload", event: "configured" }),
expect.objectContaining({ kind: "shell-attempt", phase: "blocked" }),
expect.objectContaining({ kind: "updater", phase: "blocked" }),
expect.objectContaining({ kind: "native-addon" }),
expect.objectContaining({ kind: "popup-attempt", phase: "blocked" }),
]),
);
expect(result.default_popup_decision).toEqual({ action: "deny" });
expect(result.popup_decision).toEqual({ action: "deny" });
expect(
snapshot.events.filter(
(event) => event.kind === "download" && event.phase === "blocked",
),
).toHaveLength(2);
const ipc = snapshot.events.find(
(event) => event.kind === "main-handler-invocation",
);
expect(ipc).toMatchObject({
correlation_id: expect.stringMatching(/^capture:\d+:\d+$/u),
channel_truncated: true,
argument_shapes_truncated: true,
direction: "renderer-to-main",
sender: "webContents:9",
frame: "frame:7",
});
expect(ipc?.channel).toHaveLength(1_024);
expect(ipc?.argument_shapes).toHaveLength(32);
});
it("preserves fatal process termination after recording uncaught exceptions", async () => {
const root = await createTestTempDirectory("rea-electron-hook-fatal-");
temporary.push(root);
const script = join(root, "fatal.cjs");
await writeFile(script, 'throw new Error("fatal-hook-secret");\n');
const hook = join(process.cwd(), "scripts/electron-active-hook.cjs");
const result = await runNodeStatus(hook, script, root, root);
expect(result.code).not.toBe(0);
expect(result.stderr).toContain("fatal-hook-secret");
});
const runNode = (
hook: string,
script: string,
moduleRoot: string,
cwd: string,
): Promise<string> =>
new Promise((resolve, reject) => {
const child = spawn(process.execPath, ["-r", hook, script], {
cwd,
env: {
...process.env,
NODE_PATH: moduleRoot,
REA_ELECTRON_ACTIVE_MAX_RUNTIME_EVENTS: "200",
},
stdio: ["ignore", "pipe", "pipe"],
});
const stdout: Buffer[] = [];
const stderr: Buffer[] = [];
child.stdout.on("data", (chunk: Buffer) => stdout.push(chunk));
child.stderr.on("data", (chunk: Buffer) => stderr.push(chunk));
child.once("error", reject);
child.once("close", (code) => {
if (code === 0) {
const text = Buffer.concat(stdout).toString("utf8");
if (text.length > 0) resolve(text);
else
reject(
new Error(
`hook fixture produced no output: ${Buffer.concat(stderr).toString("utf8")}`,
),
);
} else
reject(
new Error(
`hook fixture exited with ${String(code)}: ${Buffer.concat(stderr).toString("utf8")}`,
),
);
});
});
const runNodeStatus = (
hook: string,
script: string,
moduleRoot: string,
cwd: string,
): Promise<{ readonly code: number | null; readonly stderr: string }> =>
new Promise((resolve, reject) => {
const child = spawn(process.execPath, ["-r", hook, script], {
cwd,
env: {
...process.env,
NODE_PATH: moduleRoot,
REA_ELECTRON_ACTIVE_MAX_RUNTIME_EVENTS: "200",
},
stdio: ["ignore", "pipe", "pipe"],
});
const stderr: Buffer[] = [];
child.stderr.on("data", (chunk: Buffer) => stderr.push(chunk));
child.once("error", reject);
child.once("close", (code) =>
resolve({ code, stderr: Buffer.concat(stderr).toString("utf8") }),
);
});

View File

@ -93,6 +93,43 @@ describe("passive V8 Inspector provider", () => {
}
});
test("excludes an Electron main target that reports only bare file://", async () => {
const fixture = await runtimeFixture();
const fake = await startFakeV8Inspector({
targetUrl: "file://",
scriptUrls: [pathToFileURL(fixture.entry).href],
});
try {
const provider = new V8InspectorProvider();
const listed = await provider.listTargets({
inspector_endpoint: fake.endpoint,
allowed_file_roots: [fixture.root],
allowed_origins: [],
approved: true,
offset: 0,
limit: 100,
});
expect(listed.ok).toBe(true);
if (!listed.ok) return;
expect(listed.value.targets.total).toBe(0);
expect(listed.value.targets.items).toEqual([]);
const observed = await provider.observe(
observeInput(
fake.endpoint,
fake.targetId,
fixture.root,
"electron-main",
),
);
expect(observed.ok).toBe(false);
if (observed.ok) return;
expect(observed.error.message).toMatch(/outside_file_roots|target/u);
} finally {
await fake.close();
}
});
test.each([
["node", "node"],
["electron-main", "node"],

View File

@ -18,6 +18,7 @@ import {
} from "../../../src/application/InvestigationProviders.js";
import { CDP_BROWSER_PROVIDER_IDENTITY } from "../../../src/browser/CdpBrowserProvider.js";
import { CDP_ELECTRON_PROVIDER_IDENTITY } from "../../../src/browser/CdpElectronProvider.js";
import { PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY } from "../../../src/browser/PlaywrightElectronActiveProvider.js";
import { PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY } from "../../../src/browser/PlaywrightBrowserScenarioProvider.js";
import { V8_INSPECTOR_PROVIDER_IDENTITY } from "../../../src/browser/V8InspectorProvider.js";
import { CLI_COMMAND_NAMES } from "../../../src/cliCommandNames.js";
@ -81,6 +82,7 @@ describe("canonical product catalog", () => {
CDP_BROWSER_PROVIDER_IDENTITY,
PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY,
CDP_ELECTRON_PROVIDER_IDENTITY,
PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY,
V8_INSPECTOR_PROVIDER_IDENTITY,
JAVASCRIPT_APPLICATION_PROVIDER,
JAVASCRIPT_RUNTIME_RECONCILIATION_PROVIDER,
@ -102,6 +104,11 @@ describe("canonical product catalog", () => {
({ id }) => id === CDP_ELECTRON_PROVIDER_IDENTITY.id,
)?.capabilities,
).toEqual(["inspect_electron_page", "list_electron_targets"]);
expect(
catalog.providers.find(
({ id }) => id === PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY.id,
)?.capabilities,
).toEqual(["capture_electron_scenario"]);
expect(
catalog.providers.find(
({ id }) => id === JAVASCRIPT_APPLICATION_PROVIDER.id,

View File

@ -1,5 +1,5 @@
import { mkdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { mkdir, rm, symlink, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { pathToFileURL } from "node:url";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
@ -10,6 +10,7 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { composeBinarySessionFromFactory } from "../../../src/application/BinarySessionComposition.js";
import type { BinarySession } from "../../../src/application/BinarySession.js";
import type { ElectronActiveObservationPort } from "../../../src/application/ElectronActiveObservationPort.js";
import { loadConfiguredPermissionAuthority } from "../../../src/application/PermissionConfiguration.js";
import { CdpElectronProvider } from "../../../src/browser/CdpElectronProvider.js";
import { parseConfig } from "../../../src/config.js";
@ -20,6 +21,7 @@ import {
type FakeCdpBrowser,
} from "../../fixtures/fakeCdpBrowser.js";
import { writeElectronBoundaryFixture } from "../../fixtures/electronBoundaryApplication.js";
import { createElectronActiveObservationFixtureResult } from "../../fixtures/electronActiveObservationResult.js";
const browsers: FakeCdpBrowser[] = [];
const resources: Array<{ close(): Promise<unknown> }> = [];
@ -183,6 +185,109 @@ it("exposes root-confined Electron discovery and inspection as Evidence v2", asy
expect(browser.commands).toHaveLength(commandsBeforeDenial);
}, 20_000);
it("exposes active Electron scenarios through the separately granted MCP boundary", async () => {
const root = await createTestTempDirectory("rea-electron-active-mcp-");
temporary.push(root);
const applicationPath = join(root, "main.js");
await writeFile(applicationPath, "module.exports = {};\n");
const aliasedRoot = join(root, "..", `${root.split("/").at(-1)}-alias`);
await symlink(root, aliasedRoot, "dir");
temporary.push(aliasedRoot);
const capturedInputs: unknown[] = [];
const activeResult =
createElectronActiveObservationFixtureResult(applicationPath);
const provider: ElectronActiveObservationPort = {
identity: () => ({
id: "test-electron-active",
name: "Test Electron active provider",
version: "1",
}),
capture: async (input) => {
capturedInputs.push(input);
return { ok: true, value: activeResult };
},
};
const config = parseConfig({
REA_ELECTRON_AUTOMATE_ENABLED: "true",
REA_ELECTRON_AUTOMATE_AUTO_GRANT: "true",
REA_ELECTRON_AUTOMATE_EXECUTABLE_ROOTS_JSON: JSON.stringify([
dirname(process.execPath),
]),
REA_ELECTRON_AUTOMATE_APPLICATION_ROOTS_JSON: JSON.stringify([root]),
});
if (!config.ok) throw config.error;
const authority = await loadConfiguredPermissionAuthority(config.value);
if (!authority.ok) throw authority.error;
const session = composeBinarySessionFromFactory(() => ({
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(),
}));
const server = createServer(session, session, {
electronActiveObservation: provider,
permissionAuthority: authority.value,
availabilityPolicy: () => ({
processCaptureEnabled: false,
evidenceFileRoots: 0,
investigationInputRoots: 0,
electronAutomationEnabled: true,
}),
});
const client = new Client({ name: "electron-active-mcp-test", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
resources.push(client, server, session);
await server.connect(serverTransport);
await client.connect(clientTransport);
const captured = await client.callTool({
name: "capture_electron_scenario",
arguments: {
schema_version: 1,
executable_path: process.execPath,
application_path: join(aliasedRoot, "main.js"),
application_root: aliasedRoot,
args: ["--token", "super-secret"],
actions: [
{ step_id: "submit", kind: "click", selector: "#submit-secret" },
],
approved: true,
},
});
expect(captured.isError, JSON.stringify(captured)).not.toBe(true);
expect(captured.structuredContent).toMatchObject({
result: {
application: { process_ownership: "provider-owned" },
ipc: { events: [{ channel: "readiness:echo" }] },
coverage: {
status: "partial_attach",
pre_capture_activity: "unavailable",
},
},
});
expect(capturedInputs).toHaveLength(1);
expect(capturedInputs[0]).toMatchObject({
application_path: applicationPath,
application_root: root,
});
expect(JSON.stringify(captured.structuredContent)).not.toContain(
"submit-secret",
);
expect(JSON.stringify(captured.structuredContent)).not.toContain(
"super-secret",
);
const evidenceId = Reflect.get(
captured.structuredContent ?? {},
"evidence_id",
);
expect(session.evidenceById(String(evidenceId))).toMatchObject({
predicate_type: "rea.electron-active-scenario/v1",
parameters: {
args: ["--token", "<redacted>"],
actions: [{ step_id: "submit", kind: "click" }],
},
});
}, 20_000);
it("exposes the target-free static JavaScript application workflow", async () => {
const root = await createTestTempDirectory("rea-electron-static-mcp-");
temporary.push(root);

View File

@ -1,4 +1,5 @@
import { isInputRequiredResult } from "@modelcontextprotocol/server";
import { realpathSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { PermissionAuthority } from "../../../src/application/PermissionAuthority.js";
import { createPermissionPolicy } from "../../../src/domain/permissionPolicy.js";
@ -8,7 +9,7 @@ import {
} from "../../../src/server/ProcessCaptureElicitation.js";
const request = {
capability: "process_capture" as const,
roots: ["/tmp"],
roots: [realpathSync("/tmp")],
executables: [process.execPath],
environment_names: ["PATH"],
network: "external" as const,

View File

@ -1,4 +1,5 @@
import { isInputRequiredResult } from "@modelcontextprotocol/server";
import { realpathSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { PermissionAuthority } from "../../../src/application/PermissionAuthority.js";
import { createPermissionPolicy } from "../../../src/domain/permissionPolicy.js";
@ -8,7 +9,7 @@ import {
} from "../../../src/server/ProcessCaptureElicitation.js";
const request = {
capability: "process_capture" as const,
roots: ["/tmp"],
roots: [realpathSync("/tmp")],
executables: [process.execPath],
environment_names: ["PATH"],
network: "external" as const,

View File

@ -1,5 +1,6 @@
import { isInputRequiredResult } from "@modelcontextprotocol/server";
import { mkdir, rm } from "node:fs/promises";
import { realpathSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { PermissionAuthority } from "../../../src/application/PermissionAuthority.js";
@ -10,7 +11,7 @@ import {
} from "../../../src/server/ProcessCaptureElicitation.js";
const request = {
capability: "process_capture" as const,
roots: ["/tmp"],
roots: [realpathSync("/tmp")],
executables: [process.execPath],
environment_names: ["PATH"],
network: "external" as const,

View File

@ -77,10 +77,10 @@ describe("server and catalog identity", () => {
expect(SDK_IDENTITY.server).toBe(
packageJson.dependencies["@modelcontextprotocol/server"],
);
expect(CLI_COMMAND_NAMES).toHaveLength(67);
expect(new Set(CLI_COMMAND_NAMES).size).toBe(67);
expect(CLI_COMMAND_NAMES).toHaveLength(68);
expect(new Set(CLI_COMMAND_NAMES).size).toBe(68);
expect(CATALOG_IDENTITY.counts).toEqual({
cli_commands: 67,
cli_commands: 68,
mcp_tools: TOOL_CONTRACTS.length,
mcp_prompts: 6,
mcp_resources: 2,

View File

@ -59,7 +59,7 @@ function collectionTriggers() {
parent_pid: 1,
command: "rea-reactive-worker",
process_group_id: 1,
session_id: 1,
session_id: process.platform === "darwin" ? null : 1,
},
["at_ms"],
),

View File

@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest";
import {
cleanupOwnedProcessGroup,
observeOwnedProcessLineage,
verifyNoTokenOwnedProcesses,
type ProcessOwnershipHost,
} from "../../../src/process/ProcessOwnership.js";
const ownership = {
@ -34,6 +35,74 @@ const host = (
signalGroup,
};
};
describe("owned process-group cleanup discovery", () => {
it("signals token-owned groups that were reparented outside the launcher tree", async () => {
const signalGroup = vi.fn();
const adapter: ProcessOwnershipHost = {
listProcesses: () =>
Promise.resolve([
{
pid: 100,
parentPid: 1,
processGroupId: 100,
state: "S",
command: "fixture",
},
{
pid: 900,
parentPid: 1,
processGroupId: 900,
state: "S",
command: "detached-helper",
},
{
pid: 901,
parentPid: 900,
processGroupId: 900,
state: "S",
command: "detached-worker",
},
]),
environment: () => Promise.resolve({ REA_PROCESS_RUN_ID: "run-token" }),
signalGroup,
};
await expect(
cleanupOwnedProcessGroup(
{ ...ownership, sweepTokenOwnedProcesses: true },
adapter,
),
).resolves.toEqual({ cleaned: true, signaled: true });
expect(signalGroup.mock.calls.map(([groupId]) => groupId)).toEqual([
100, 900,
]);
});
it("fails closed when a token-owned process remains after cleanup", async () => {
const adapter: ProcessOwnershipHost = {
listProcesses: () =>
Promise.resolve([
{
pid: 900,
parentPid: 1,
processGroupId: 900,
state: "S",
command: "detached-helper",
},
]),
environment: () => Promise.resolve({ REA_PROCESS_RUN_ID: "run-token" }),
signalGroup: vi.fn(),
};
await expect(
verifyNoTokenOwnedProcesses("run-token", adapter),
).resolves.toEqual({
cleaned: false,
reason: "token-owned process remained after cleanup",
});
});
});
describe("owned process-group cleanup discovery", () => {
it("fails before signaling when any descendant-group member is unowned", async () => {
const signalGroup = vi.fn();

View File

@ -67,6 +67,15 @@ describe("provider process runtime and wait primitives", () => {
});
});
it("fails closed for a Windows runtime root without native DACL authority", async () => {
await expect(
PrivateRuntimeRoot.create({ platform: "win32" }),
).rejects.toMatchObject({
code: "private-runtime-root-authority-unavailable",
message: expect.stringContaining("chmod(0700)"),
});
});
it("clears the startup timer and external cancellation listener", async () => {
vi.useFakeTimers();
const controller = new AbortController();
@ -335,6 +344,45 @@ describe("provider process spawning primitives", () => {
},
);
it.skipIf(process.platform === "win32")(
"stops a detached fixture descendant when its launcher receives SIGTERM",
async () => {
const launcher = spawnProviderProcessFixture("detached-child");
let detachedChildPid: number | undefined;
const cleanup = async (): Promise<void> => {
await stopProviderProcessFixture(launcher);
if (detachedChildPid === undefined) return;
try {
process.kill(detachedChildPid, "SIGKILL");
} catch (cause: unknown) {
if (
!(
cause instanceof Error &&
"code" in cause &&
cause.code === "ESRCH"
)
)
throw cause;
}
};
try {
detachedChildPid = await waitForDetachedProviderChild(launcher);
const launcherPid = launcher.pid;
if (launcherPid === undefined)
throw new Error("Detached fixture launcher did not expose a PID");
launcher.kill("SIGTERM");
await Promise.all([
waitForPidExit(launcherPid),
waitForPidExit(detachedChildPid),
]);
} catch (cause: unknown) {
await cleanup();
throw cause;
}
await cleanup();
},
);
it("allows interpreter launchers to rely on parent and run-token identity", async () => {
const spawned = await spawnOwnedProviderProcess({
command: process.execPath,

View File

@ -61,21 +61,22 @@ class FixtureLauncher implements GhidraLauncher {
this.tokens.push(session.token);
const projectRoot = join(session.runtimeRoot, "project");
await mkdir(projectRoot, { recursive: true });
const process_ = spawn(
process.execPath,
[
fixturePath,
session.endpointPath,
session.token,
session.runId,
session.providerVersion,
session.profileDigest,
session.targetSha256,
session.transport,
this.mode,
],
{ stdio: ["ignore", "pipe", "pipe"] },
);
const process_ = spawn(process.execPath, [fixturePath], {
stdio: ["ignore", "pipe", "pipe"],
env: {
...process.env,
REA_GHIDRA_FIXTURE_CONFIG: JSON.stringify({
endpointPath: session.endpointPath,
token: session.token,
runId: session.runId,
providerVersion: session.providerVersion,
profileDigest: session.profileDigest,
targetSha256: session.targetSha256,
transport: session.transport,
mode: this.mode,
}),
},
});
this.processes.push(process_);
return ok({
process: process_,

View File

@ -142,7 +142,7 @@ describe("Ghidra provider", () => {
});
describe("Ghidra platform support", () => {
it("admits only native x86-64 PE applications on Windows P0", () => {
it("keeps Windows P0 unavailable until native isolation authority exists", () => {
const ghidra = provider({ ...installationHost(), platform: "win32" });
const nativeApplication: BinaryTarget = {
...executableTarget("pe", "x86_64"),
@ -158,12 +158,35 @@ describe("Ghidra platform support", () => {
managed: false,
},
});
expect(ghidra.inspectAvailability()).toMatchObject({
status: "unavailable",
code: "unsupported_host",
reason: expect.stringContaining("native authority is unavailable"),
diagnostics: {
windows_security: {
job_object_process_ownership: expect.objectContaining({
available: false,
proof: "not-proven",
}),
private_runtime_dacl: expect.objectContaining({
available: false,
proof: "not-proven",
}),
reparse_safe_path_admission: expect.objectContaining({
available: false,
proof: "not-proven",
}),
},
},
});
expect(ghidra.capabilities()).toEqual(
expect.arrayContaining([
expect.objectContaining({
operation: "list_procedures",
available: false,
availabilityCode: "unsupported_host",
limitations: expect.arrayContaining([
expect.stringContaining("bounded taskkill termination"),
expect.stringContaining("bounded taskkill cleanup"),
expect.stringContaining("private DACL enforcement"),
]),
}),

View File

@ -182,7 +182,7 @@ describe("Linux private display selection", () => {
);
it("reports missing helper dependencies without exposing raw stderr", () => {
for (const [option, reason] of [
for (const [option] of [
["--xauth", "missing_xauth"],
["--xvfb", "missing_xvfb"],
] as const) {
@ -198,9 +198,10 @@ describe("Linux private display selection", () => {
value: {
status: "error",
failure_code: "runtime_dependency_unavailable",
reason,
},
});
if (parsed.ok)
expect(parsed.value.reason).toMatch(/^missing_(xauth|xvfb)$/u);
expect(result.stderr).not.toContain("Traceback");
}
});

View File

@ -51,6 +51,7 @@ describe("tool contract inventory", () => {
"inspect_electron_page",
"analyze_javascript_application",
"reconcile_javascript_runtime",
"capture_electron_scenario",
]);
expect(APPLICATION_TOOL_CONTRACTS.map(({ name }) => name)).toEqual([
"trace_application_feature",

View File

@ -6,11 +6,14 @@ import { afterEach, expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { createElectronEvidence } from "../../../src/application/ElectronEvidence.js";
import { createElectronActiveEvidence } from "../../../src/application/ElectronActiveEvidence.js";
import { analyzeJavaScriptApplication } from "../../../src/application/JavaScriptApplicationService.js";
import { reconcileJavaScriptRuntime } from "../../../src/domain/javascriptRuntimeReconciliation.js";
import { javascriptRuntimeReconciliationResultSchema } from "../../../src/domain/javascriptRuntimeReconciliationSchemas.js";
import { electronActiveObservationInputSchema } from "../../../src/domain/electronActiveObservation.js";
import { createWebTextArtifact } from "../../../src/domain/webContentArtifact.js";
import { permissionAuthorityForRoot } from "../../fixtures/permissionAuthority.js";
import { createElectronActiveObservationFixtureResult } from "../../fixtures/electronActiveObservationResult.js";
const SOURCE = `const worker = new Worker("./worker.js");\nexport const observed = worker;\n`;
@ -142,6 +145,87 @@ it("keeps source-map declarations outside primary matching authority", async ()
});
});
it("reconciles active Electron as a partial target-only runtime capture", async () => {
const fixture = await applicationFixture();
temporary.push(fixture);
const staticEvidence = await analyzeFixture(fixture);
const applicationPath = join(fixture, "main.js");
const input = electronActiveObservationInputSchema.parse({
schema_version: 1,
executable_path: process.execPath,
application_path: applicationPath,
application_root: fixture,
actions: [],
approved: true,
});
const runtimeEvidence = createElectronActiveEvidence(
input,
createElectronActiveObservationFixtureResult(applicationPath),
{
id: "rea-playwright-electron-active",
name: "REA Playwright active Electron observation provider",
version: "1",
},
);
const result = reconcileJavaScriptRuntime({
static_layers: [{ role: "application", analysis: staticEvidence }],
runtime_observations: [runtimeEvidence],
});
expect(result.runtime_captures).toMatchObject([
{
kind: "electron-active",
scripts: 0,
frames: 0,
workers: 0,
scripts_complete_within_scope: false,
},
]);
expect(result.coverage.status).toBe("partial");
expect(result.limitations.join(" ")).toMatch(
/incomplete|not reported as executed/u,
);
});
it("hashes the retained Electron scenario projection, not raw selectors or secrets", async () => {
const fixture = await applicationFixture();
temporary.push(fixture);
const applicationPath = join(fixture, "main.js");
const makeInput = (secret: string, selector: string) =>
electronActiveObservationInputSchema.parse({
schema_version: 1,
executable_path: process.execPath,
application_path: applicationPath,
application_root: fixture,
args: ["--token", secret],
actions: [{ step_id: "click", kind: "click", selector }],
approved: true,
});
const provider = {
id: "rea-playwright-electron-active",
name: "REA Playwright active Electron observation provider",
version: "1",
};
const first = createElectronActiveEvidence(
makeInput("first-secret", "#first-secret"),
createElectronActiveObservationFixtureResult(applicationPath),
provider,
);
const second = createElectronActiveEvidence(
makeInput("second-secret", "#second-secret"),
createElectronActiveObservationFixtureResult(applicationPath),
provider,
);
expect(first.parameters.scenario_sha256).toBe(
second.parameters.scenario_sha256,
);
expect(JSON.stringify(first)).not.toContain("first-secret");
expect(JSON.stringify(first)).not.toContain("#first-secret");
expect(first.parameters.args).toEqual(["--token", "<redacted>"]);
});
it("imports an operator-provided cache layer through an explicit file mapping", async () => {
const application = await applicationFixture();
const cache = await createTestTempDirectory("rea-runtime-cache-static-");

View File

@ -42,6 +42,11 @@ describe("runtime configuration", () => {
expect(result.value.electronObservationEnabled).toBe(false);
expect(result.value.electronCdpEndpoints).toEqual([]);
expect(result.value.electronFileRoots).toEqual([]);
expect(result.value.electronAutomationPolicy).toEqual({
enabled: false,
executableRoots: [],
applicationRoots: [],
});
expect(result.value.v8InspectorObservationEnabled).toBe(false);
expect(result.value.v8InspectorEndpoints).toEqual([]);
expect(result.value.v8InspectorFileRoots).toEqual([]);
@ -144,7 +149,64 @@ describe("runtime permission configuration", () => {
mount: false,
});
});
});
describe("active Electron permission configuration", () => {
it("builds a separately granted, no-network Electron automation ceiling", () => {
const result = parseConfig({
REA_ELECTRON_AUTOMATE_ENABLED: "true",
REA_ELECTRON_AUTOMATE_AUTO_GRANT: "false",
REA_ELECTRON_AUTOMATE_EXECUTABLE_ROOTS_JSON: '["/opt/electron"]',
REA_ELECTRON_AUTOMATE_APPLICATION_ROOTS_JSON: '["/tmp/apps"]',
});
if (!result.ok) throw result.error;
expect(result.value.electronAutomationPolicy).toEqual({
enabled: true,
executableRoots: ["/opt/electron"],
applicationRoots: ["/tmp/apps"],
});
expect(result.value.permissionCeilings).toContainEqual({
capability: "electron_automate",
roots: ["/tmp/apps"],
executables: ["/opt/electron"],
environment_names: [],
network: "external",
mount: false,
});
expect(result.value.administratorPermissionGrants).not.toContainEqual(
expect.objectContaining({ capability: "electron_automate" }),
);
const granted = parseConfig({
REA_ELECTRON_AUTOMATE_ENABLED: "true",
REA_ELECTRON_AUTOMATE_AUTO_GRANT: "true",
REA_ELECTRON_AUTOMATE_EXECUTABLE_ROOTS_JSON: '["/opt/electron"]',
REA_ELECTRON_AUTOMATE_APPLICATION_ROOTS_JSON: '["/tmp/apps"]',
});
if (!granted.ok) throw granted.error;
expect(granted.value.administratorPermissionGrants).toContainEqual(
expect.objectContaining({
capability: "electron_automate",
lifetime: "administrator",
}),
);
});
it("rejects relative active Electron roots", () => {
expect(
parseConfig({
REA_ELECTRON_AUTOMATE_APPLICATION_ROOTS_JSON: '["relative/apps"]',
}).ok,
).toBe(false);
expect(
parseConfig({
REA_ELECTRON_AUTOMATE_EXECUTABLE_ROOTS_JSON: '["relative/bin"]',
}).ok,
).toBe(false);
});
});
describe("runtime permission configuration", () => {
it("can configure a process-capture ceiling without an implicit grant", () => {
const result = parseConfig({
REA_PROCESS_CAPTURE_ENABLED: "true",

View File

@ -5,6 +5,7 @@ import {
createConformancePackage,
type ConformancePackage,
} from "../../../src/domain/conformancePackage.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import {
replayConformancePackage,
type ScenarioReplayResult,
@ -115,6 +116,41 @@ describe("conformance CI replay", () => {
expect(result.drift_detected).toBe(false);
});
it("retains the observed Evidence ID on the first drift", async () => {
const expected = createEvidence(
undefined,
{ id: "fixture-provider", name: "Fixture provider", version: "1" },
{ operation: "run", parameters: {}, result: { exit_code: 0 } },
);
const pkg = createConformancePackage({
...validPackage,
expected_evidence: [
{
scenario_id: "s1",
envelopes: [expected],
bundle: null,
required_dimensions: ["exit_code"],
},
],
});
const actualEvidenceId = `ev_${"3".repeat(64)}`;
const result = await replayConformancePackage(
pkg,
{
s1: {
evidence_id: actualEvidenceId,
normalized_result: { exit_code: 1 },
},
},
passingRunner,
);
expect(result.drift_detected).toBe(true);
expect(result.first_drift).toMatchObject({
dimension: "exit_code",
evidence_id: actualEvidenceId,
});
});
it("throws on invalid package", async () => {
await expect(replayConformancePackage({}, {})).rejects.toThrow(
/invalid conformance package/u,

View File

@ -62,7 +62,78 @@ describe("conformance trust gates", () => {
it("handles missing actual", () => {
const result = compareDimension("exit_code", 0, undefined);
expect(result.status).toBe("mismatch");
expect(result.status).toBe("unknown");
});
it("keeps semantic unknown and truncated values out of equivalence", () => {
expect(
compareDimension(
"process_tree",
{ processes: [] },
{ status: "unknown" },
),
).toMatchObject({ status: "unknown" });
expect(
compareDimension("process_tree", { processes: [] }, { truncated: true }),
).toMatchObject({ status: "truncated" });
expect(
compareDimension(
"process_tree",
{ status: "unknown" },
{ status: "unknown" },
),
).toMatchObject({ status: "unknown" });
});
it("projects Evidence envelopes and links a divergence to the observed ID", () => {
const expectedEvidenceId = `ev_${"1".repeat(64)}`;
const actualEvidenceId = `ev_${"2".repeat(64)}`;
const result = evaluateTrustGate(
contract,
{
evidence_id: expectedEvidenceId,
normalized_result: { exit_code: 0, stdout: "hello" },
},
{
evidence_id: actualEvidenceId,
normalized_result: { exit_code: 1, stdout: "hello" },
},
);
expect(result.verdict).toBe("fail");
expect(result.dimension_results[0]?.evidence_ids).toEqual([
actualEvidenceId,
expectedEvidenceId,
]);
expect(result.first_divergence).toMatchObject({
dimension: "exit_code",
evidence_id: actualEvidenceId,
});
});
it("scopes process residual unknowns without hiding complete dimensions", () => {
const processContract: VerifierContract = {
...contract,
dimensions: [
{ name: "exit_code", required: true, comparison: "exact" },
{ name: "process_tree", required: true, comparison: "semantic" },
],
};
const result = evaluateTrustGate(
processContract,
{ exit_code: 0, process_tree: { processes: [] } },
{
exit_code: 0,
process_tree: { processes: [] },
residual_unknowns: [{ scope: "process", reason: "event gap" }],
},
);
expect(result.verdict).toBe("unknown");
expect(result.dimension_results).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: "exit_code", status: "match" }),
expect.objectContaining({ name: "process_tree", status: "unknown" }),
]),
);
});
it("passes when all dimensions match", () => {

View File

@ -5,6 +5,7 @@ import {
systemNoFollowOpenCapability,
type WindowsCapabilityDependencies,
} from "../../../../src/application/WindowsCapabilities.js";
import { WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON } from "../../../../src/process/WindowsAuthority.js";
const dependencies = (
overrides: Partial<WindowsCapabilityDependencies> = {},
@ -67,6 +68,23 @@ describe("Windows host capability report", () => {
},
pty: { available: false, reason: "probe unavailable" },
},
security: {
job_object_process_ownership: {
available: false,
reason: WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON,
proof: "not-proven",
},
private_runtime_dacl: {
available: false,
reason: WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON,
proof: "not-proven",
},
reparse_safe_path_admission: {
available: false,
reason: WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON,
proof: "not-proven",
},
},
});
});
@ -89,5 +107,26 @@ describe("Windows host capability report", () => {
reason: null,
});
expect(result.capabilities.private_acl.available).toBe(false);
expect(result.security.private_runtime_dacl).toMatchObject({
available: false,
proof: "not-proven",
});
});
it("does not promote an ordinary ACL probe into native authority", async () => {
const result = await probeWindowsCapabilities(
dependencies({
probePrivateAcl: () => ({ available: true, reason: null }),
}),
);
expect(result.capabilities.private_acl).toEqual({
available: true,
reason: null,
});
expect(result.security.private_runtime_dacl).toMatchObject({
available: false,
proof: "not-proven",
});
});
});

View File

@ -5,19 +5,32 @@ const {
shell,
utilityProcess,
} = require("electron");
const { spawn } = require("node:child_process");
const windows = new Set();
app.on("open-url", (_event, url) => {
const deliverDeepLink = (url) => {
for (const window of windows) window.webContents.send("deep-link", url);
};
app.on("open-url", (_event, url) => deliverDeepLink(url));
app.on("second-instance", (_event, commandLine) => {
const url = commandLine.at(-1);
if (typeof url === "string") deliverDeepLink(url);
});
app.whenReady().then(() => {
const window = new BrowserWindow({
webPreferences: { preload: require.resolve("./preload.js") },
});
windows.add(window);
window.loadFile("renderer.html");
for (let index = 0; index < 2; index += 1) {
const window = new BrowserWindow({
webPreferences: { preload: require.resolve("./preload.js") },
});
windows.add(window);
window.loadFile("renderer.html");
window.webContents.on("render-process-gone", () => window.reload());
}
utilityProcess.fork(require.resolve("./utility.js"));
spawn(
process.env.REA_ELECTRON_NODE_EXECUTABLE ?? "node",
["-e", "setTimeout(() => {}, 100)"],
{ stdio: "ignore" },
);
ipcMain.handle("readiness:echo", (_event, value) => ({ value }));
window.webContents.on("render-process-gone", () => window.reload());
shell.openExternal("https://blocked.invalid").catch(() => undefined);
});

View File

@ -107,4 +107,12 @@ describe("localized README product facts", () => {
"pin persistent MCP registrations to the exact version",
);
});
it("documents the published MCP Registry installation path", async () => {
const content = await readFile(resolve("docs/installation.md"), "utf8");
expect(content).toContain("MCP Registry");
expect(content).toContain("io.github.morluto/rea");
expect(content).toContain('"command": "npx"');
expect(content).toContain('"args": ["-y", "rea-agents@latest", "mcp"]');
});
});

View File

@ -26,6 +26,16 @@ describe("release configuration", () => {
type: "generic",
path: "src/generatedPackageMetadata.ts",
},
{
type: "json",
path: "server.json",
jsonpath: "$.version",
},
{
type: "json",
path: "server.json",
jsonpath: "$.packages[0].version",
},
{
type: "json",
path: "docs/product-catalog.json",
@ -61,6 +71,47 @@ describe("release configuration", () => {
expect(generated).toContain("x-release-please-version");
});
it("keeps the npm package and MCP Registry metadata aligned", async () => {
const packageJson = (await readJson("package.json")) as {
name: string;
version: string;
mcpName: string;
};
const serverJson = (await readJson("server.json")) as {
name: string;
version: string;
packages: Array<{
registryType: string;
registryBaseUrl: string;
identifier: string;
version: string;
runtimeHint: string;
transport: { type: string };
packageArguments: Array<{ type: string; value: string }>;
}>;
};
expect(packageJson).toMatchObject({
name: "rea-agents",
mcpName: "io.github.morluto/rea",
});
expect(serverJson).toMatchObject({
name: packageJson.mcpName,
version: packageJson.version,
packages: [
{
registryType: "npm",
registryBaseUrl: "https://registry.npmjs.org",
identifier: packageJson.name,
version: packageJson.version,
runtimeHint: "npx",
transport: { type: "stdio" },
packageArguments: [{ type: "positional", value: "mcp" }],
},
],
});
});
it("keeps TypeDoc unversioned and outside the tracked documentation tree", async () => {
await expect(readJson("typedoc.json")).resolves.toMatchObject({
includeVersion: false,

View File

@ -0,0 +1,61 @@
import { electronActiveObservationResultSchema } from "../../src/domain/electronActiveObservation.js";
/** Create a bounded active-capture result for MCP contract tests. */
export const createElectronActiveObservationFixtureResult = (
applicationPath: string,
) =>
electronActiveObservationResultSchema.parse({
schema_version: 1,
application: {
executable_path: process.execPath,
application_path: applicationPath,
electron_version: "test-electron",
process_ownership: "provider-owned",
cleanup: "terminated-owned-process",
},
actions: [
{
step_id: "submit",
kind: "click",
window_index: 0,
target: "window:1",
status: "completed",
elapsed_ms: 3,
error: null,
},
],
windows: [
{
window_id: "window:1",
web_contents_id: "webContents:1",
url: "file:///tmp/app.html",
title: "Fixture",
visible: true,
destroyed: false,
},
],
windows_truncated: false,
processes: {
items: [{ pid: 1234, type: "Browser", name: null, service_name: null }],
truncated: false,
},
ipc: {
events: [
{
sequence: 1,
kind: "main-handler-invocation",
channel: "readiness:echo",
argument_shapes: ["string"],
result_shape: "object",
process_type: "main",
error: false,
},
],
observed: 1,
retained: 1,
truncated: false,
},
limitations: [
"IPC payloads are represented only by bounded value shapes; values are never retained.",
],
});

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