Compare commits

...
Author SHA1 Message Date
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
344b9b6dff chore(main): release rea-agents 3.1.0 (#463)
* chore(main): release rea-agents 3.1.0

* chore: normalize release product catalog

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-10 03:47:06 +08:00
morlutoandGitHub bf04d1aea3 fix(release): let release PR updates trigger CI (#470) 2026-08-10 03:46:53 +08:00
morlutoandGitHub 8b62b1a715 docs(npm): simplify current setup command (#469) 2026-08-10 03:24:24 +08:00
morlutoandGitHub 1783bf6dfd fix(npm): preserve explicit setup versions (#465)
* fix(npm): refresh stale setup bootstraps

* fix(npm): preserve explicit setup versions

* docs: distinguish first-run and refresh setup
2026-08-10 02:59:52 +08:00
morlutoandGitHub ed4a62871f docs: clarify the agent reverse-engineering promise (#466)
docs: clarify agent reverse-engineering promise
2026-08-10 02:46:31 +08:00
morlutoandGitHub b8a9da0c0f refactor(mcp): make analysis states unrepresentable (#468)
* refactor(config): parse capability settings into exact states

* refactor(runtime): encode replay and process outcomes exactly

* refactor: make analysis states unrepresentable

* refactor(mcp): register canonical schemas with SDK

* fix: preserve capability and catalog invariants

* fix(build): remove stale compiled modules

* fix(replay): update real verifier policy dependency

* fix(package): verify the complete MCP catalog

* fix(test): supply complete replay reload policy

* refactor(contracts): centralize output schema lookup

* refactor(mcp): delegate protocol boundaries to SDK

* fix(browser): approve real source capture

* fix(mcp): preserve catalog guidance
2026-08-10 02:28:11 +08:00
morlutoandGitHub 6901edc05d fix: harden structured configuration comparisons (#467)
* refactor(process): split ownership observation helpers

* fix: compare structured configuration canonically

* test(conformance): tolerate Markdown line wrapping

* refactor: name caught values explicitly
2026-08-09 11:58:01 +08:00
morlutoandGitHub 8f229ede70 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
2026-08-05 19:07:46 +08:00
morlutoandGitHub 51559245d2 Merge pull request #461 from morluto/agent/comprehensive-repository-audit
fix: harden evidence lifecycle and MCP contracts
2026-08-04 03:00:25 +08:00
morlutoandGitHub 84cc819284 Merge pull request #462 from morluto/agent/fix-release-please-output
fix(ci): guard Release Please PR branch parsing
2026-08-04 01:56:20 +08:00
morluto 6838b6bff9 fix(ci): guard release PR branch parsing 2026-08-04 01:44:28 +08:00
morlutoandGitHub 960b4df056 Merge pull request #455 from morluto/release-please--branches--main--components--rea-agents
chore(main): release rea-agents 3.0.0
2026-08-03 00:23:01 +08:00
github-actions[bot] a83dbb2fb3 chore: normalize release product catalog 2026-08-01 07:18:24 +00:00
github-actions[bot]andGitHub 71f03a7224 chore(main): release rea-agents 3.0.0 2026-08-01 07:17:25 +00:00
morlutoandGitHub 7b6f7becd2 Merge pull request #459 from morluto/agent/verify-npx-upgrade-e2e
test(package): verify npx upgrade on published releases
2026-08-01 15:16:45 +08:00
morluto 1f8ad7bc68 test(package): verify npx upgrade on published releases 2026-08-01 15:10:12 +08:00
morlutoandGitHub 7f46bb1824 Merge pull request #458 from morluto/fix/test-subprocess-coverage
fix(test): retain subprocess coverage and cleanup
2026-07-30 12:54:21 +08:00
morluto dae2b9cc4e fix(test): retain subprocess coverage and cleanup 2026-07-30 04:50:34 +00:00
morlutoandGitHub 48053d1032 Merge pull request #457 from morluto/agent/test-suite-architecture-overhaul
test: overhaul deterministic suite architecture
2026-07-30 12:43:07 +08:00
morluto f0d8cdafa8 fix(test): stabilize boundary timing 2026-07-30 04:37:41 +00:00
morluto 97f6a0f3fd fix(ci): allow documented public type aliases 2026-07-30 04:30:10 +00:00
morluto 92a1c33625 fix(test): honor boundary ownership and public aliases 2026-07-30 04:25:52 +00:00
morluto 8ccdb33dd2 test(contracts): cover validation branches 2026-07-30 04:20:10 +00:00
morluto 788f4057ae fix(ci): make coverage report merging composable 2026-07-30 04:04:45 +00:00
morluto f07acde513 fix(ci): stabilize Vitest project routing and shards 2026-07-30 03:59:44 +00:00
morluto f47ee1abb4 fix(ci): align Vitest project worker budgets 2026-07-29 22:24:54 +00:00
morluto 935721df10 fix(test): bound boundary worker pressure 2026-07-29 22:16:16 +00:00
morluto 3c42403b78 ci: document deterministic suite ownership 2026-07-29 22:02:24 +00:00
morluto a8161f0e55 refactor: preserve provider-neutral runtime seams 2026-07-29 22:02:14 +00:00
morluto 513d9b1555 test: overhaul deterministic suite architecture 2026-07-29 22:00:38 +00:00
morlutoandGitHub 7f2348619a Merge pull request #456 from morluto/agent/mcp-resource-snapshots-context-tools
feat!: make evidence bundles canonical MCP resources
2026-07-30 02:22:51 +08:00
morluto ede91ff1f0 fix: align aggregate tools with session capabilities 2026-07-29 18:18:31 +00:00
morluto f3b3fb07de fix: address MCP resource review feedback 2026-07-29 18:10:45 +00:00
morluto ab4193c1f4 test: derive MCP SDK identity expectations 2026-07-29 17:54:33 +00:00
morluto c824db1ab7 docs: refresh generated SDK metadata 2026-07-29 17:48:20 +00:00
morluto 17d7dbc461 build: upgrade MCP SDK to 2.0.0 2026-07-29 17:42:43 +00:00
morluto 2a45d8de7d docs: document resource snapshot workflow 2026-07-29 17:37:00 +00:00
morluto ba098f24e5 test(eval): cover navigation and address tool routing 2026-07-29 17:36:51 +00:00
morluto 1712e6d821 feat!: make evidence bundles canonical MCP resources
Add immutable session bundle snapshots, bounded resource discovery, paginated unknowns, mutable current analysis snapshots, and provider-neutral navigation/address context tools.

BREAKING CHANGE: export_evidence_bundle now requires a filesystem path and no longer returns an inline bundle.
2026-07-29 17:36:40 +00:00
morlutoandGitHub 3e1e8cd3f8 Merge pull request #454 from morluto/fix/mcp-routing-diagnostics
fix(mcp): clarify target routing and caller diagnostics
2026-07-29 15:32:51 +08:00
morluto 59d465971e docs: refresh managed completion manifest 2026-07-29 07:22:34 +00:00
morluto 281e41f766 fix(verify): follow dynamic Hopper tool availability 2026-07-29 07:18:48 +00:00
morluto 0f96bf365c fix(errors): preserve actionable analysis diagnostics 2026-07-29 07:18:29 +00:00
morluto 969519c00f fix(mcp): clarify routing and advertise input examples 2026-07-29 07:18:09 +00:00
morlutoandGitHub 222ce83967 Merge pull request #444 from morluto/release-please--branches--main--components--rea-agents
chore(main): release rea-agents 2.7.0
2026-07-28 15:02:26 +08:00
github-actions[bot] 7929bf1688 chore: normalize release product catalog 2026-07-28 06:54:15 +00:00
github-actions[bot]andGitHub 8ab40974d3 chore(main): release rea-agents 2.7.0 2026-07-28 06:53:17 +00:00
morlutoandGitHub 91fb87d17e Merge pull request #445 from morluto/codex/conformance-package-336
feat(conformance): add portable conformance export package format
2026-07-28 14:52:18 +08:00
morlutoandGitHub 54153ed62c Merge pull request #446 from morluto/codex/protocol-capture-364
feat(protocol): add gRPC/Protobuf and JSON-RPC/MessagePack capture
2026-07-28 14:52:13 +08:00
morlutoandGitHub f942558573 Merge pull request #447 from morluto/codex/pe-coff-inspection-362
feat(pe): add broader PE/COFF/PDB static inspection
2026-07-28 14:52:08 +08:00
morlutoandGitHub 1fb22b4c21 Merge pull request #448 from morluto/codex/custom-protocol-capture-365
feat(protocol): add custom TCP/UDP/IPC/XPC and auth-flow capture
2026-07-28 14:51:58 +08:00
morlutoandGitHub 1a75f8dc23 Merge pull request #449 from morluto/codex/jvm-python-bytecode-367
feat(bytecode): add JVM and Python bytecode providers
2026-07-28 14:51:17 +08:00
morlutoandGitHub 18624df572 Merge pull request #451 from morluto/codex/mobile-providers-366
feat(mobile): add Android APK/AAB/DEX and iOS IPA static investigation providers
2026-07-28 14:51:11 +08:00
morlutoandGitHub 0ef1b91cf3 Merge pull request #450 from morluto/codex/msix-msi-appx-363
feat(package): add MSIX/MSI/AppX package, resource, and signature analysis
2026-07-28 14:51:07 +08:00
morlutoandGitHub 64f64fec30 Merge branch 'main' into codex/pe-coff-inspection-362 2026-07-28 14:26:38 +08:00
morlutoandGitHub 4602efd27c Merge pull request #452 from morluto/codex/objc-swift-metadata-353
feat(metadata): add deeper ObjC/Swift metadata and database save/licensing
2026-07-28 14:26:25 +08:00
morluto 94e6600faf feat(pe): add broader PE/COFF/PDB static inspection
Expand PE/COFF/PDB inspection to cover imports, exports, resources,
manifests, signatures, debug identifiers, sections, relocations, TLS
callbacks, and architecture metadata (#362).

- peInspection.ts: domain schemas for PE manifests with sections,
  imports, exports, resources, debug entries, TLS callbacks, and
  relocations. Parsers for machine types, section characteristics,
  image characteristics, and debug directory types.
- Public contracts do not expose macOS-only vocabulary.
- Unsupported capabilities are reported explicitly.

Closes #362
2026-07-28 14:22:04 +08:00
morlutoandGitHub 4d228b4d8c Merge branch 'main' into codex/objc-swift-metadata-353 2026-07-28 14:21:35 +08:00
morlutoandGitHub a5e355a3d5 Merge pull request #453 from morluto/codex/event-process-tree-332
feat(process): replace procfs sampling with event-backed process-tree capture
2026-07-28 14:21:25 +08:00
morluto fdeeb27f23 feat(mobile): add Android APK/AAB/DEX and iOS IPA static investigation providers
Add static providers for Android APK/AAB/DEX and Apple IPA artifacts,
including manifests, resources, signing, and native libraries (#366).

- mobileApplicationInvestigation.ts: domain schemas for Android
  (APK, AAB, DEX) and iOS (IPA) artifact formats with manifests,
  signing info, resources, and native libraries.
- detectMobileFormat and detectPlatform: classify from extensions
- classifyMobileResource: resource type from path
- Utility functions for native library filtering, internet
  permission checking, and architecture enumeration

Closes #366
2026-07-28 05:54:34 +00:00
morluto d5d2e56d04 feat(process): replace procfs sampling with event-backed process-tree capture
Replace 50 ms procfs sampling with event-backed capture of root/child/
grandchild spawn, exit, re-exec, and reparent events (#332).

- eventProcessTree.ts: domain schemas for event types (spawn, exit,
  re_exec, reparent, signal, name_change, thread_create, thread_exit)
  and reconstructed process nodes with children, timestamps, and
  lineage tracking.
- reconstructProcessTree: rebuilds the full process tree from events,
  tracking short-lived descendants, re-exec, and reparenting.
- Utility functions: getDescendants, containsProcess, maxTreeDepth.
- Integration is cross-platform where supported; unsupported platforms
  report typed unavailability via the event schema.

Closes #332
2026-07-28 05:47:50 +00:00
morluto d7a07535b0 feat(metadata): add deeper ObjC/Swift metadata and database save/licensing
Add deeper Objective-C and Swift metadata extraction, plus database
path/save/licensing behavior where licensing permits (#353).

- objcSwiftMetadata.ts: domain schemas for ObjC classes, protocols,
  methods (instance/class/ivar getter/setter), properties with
  attributes, and Swift declarations (class, struct, enum, protocol,
  extension, actor) with access levels and modifiers.
- Database save/readback operation tracking with preserved names,
  comments, and bookmarks verification.
- Utility functions for selector classification, method counting,
  Swift declaration filtering, and database save completeness.
- Read-only and mutating operations are distinguished.

Closes #353
2026-07-28 05:46:10 +00:00
morluto 6c03f83b1b feat(package): add MSIX/MSI/AppX package, resource, and signature analysis
Add static analysis for MSIX, MSI, AppX packages, their resources,
and digital signatures (#363).

- packageAnalysis.ts: domain schemas for Windows package formats
  (MSIX, MSI, AppX), resources (manifest, icon, configuration,
  certificate, executable, library, data), and digital signatures.
- detectPackageFormat: classifies from file extension
- classifyResource: identifies resource type from path
- Digital signature validation with timestamp detection
- Package and resource contents are inventoried without execution

Closes #363
2026-07-28 05:42:30 +00:00
morluto 4e0127d620 feat(bytecode): add JVM and Python bytecode providers
Add execution-free providers for JVM bytecode/JAR/WAR and Python
bytecode/wheels/zipapps (#367).

- bytecodeProvider.ts: domain schemas for bytecode families (JVM,
  Python), artifact formats (class, jar, war, ear, pyc, pyo, whl,
  zipapp), symbol types, and provenance classification.
- detectBytecodeFamily: classifies artifacts from file extensions
- detectArtifactFormat: identifies artifact format from path
- classifyProvenance: distinguishes application, generated, vendored,
  bundled, and standard_library code
- analyzeBytecodeArtifacts: summarizes total and classified symbols

Closes #367
2026-07-28 05:42:05 +00:00
morluto 6fa6f9b6d9 feat(protocol): add custom TCP/UDP/IPC/XPC and auth-flow capture
Add support for custom TCP and UDP framing, local IPC, Unix sockets,
named pipes, XPC, and authentication/token lifecycle metadata (#365).

- customProtocolCapture.ts: domain schemas for custom protocol
  frames with transport types (TCP, UDP, IPC, Unix socket, named
  pipe, XPC), frame direction, process identity correlation, and
  decoded content.
- Authentication flow events with stage tracking (init, challenge,
  response, token exchange, renewal, revocation, failure, success).
- Token lifecycle events (issued, refreshed, expired, revoked,
  renewed).
- Credential detection and redaction by default.
- Filters for transport type, protocol, and auth success/failure.

Closes #365
2026-07-28 05:41:41 +00:00
morluto 3f336fefb1 feat(protocol): add gRPC/Protobuf and JSON-RPC/MessagePack capture
Add capture and schema inference for gRPC/Protocol Buffers and
JSON-RPC/MessagePack protocols (#364).

- classifyProtocolFamily: detects protocol from content type or path
- decodeJsonRpc: decodes JSON-RPC messages with credential redaction
- decodeMessagePack: decodes MessagePack wire format (map/array detection)
- decodeGrpcFrame: decodes gRPC 5-byte frame headers
- inferMessageSchema: infers schema hypotheses from repeated messages
- Credential-like fields are detected and redacted by default

Closes #364
2026-07-28 05:40:49 +00:00
morluto 4ee30e4145 feat(conformance): add portable conformance export package format
Define a committed scenario/replay/shim/evidence package format for
portable process-fidelity conformance fixtures (#336). The package
format is versioned, path-independent, and consumable from a clean
checkout.

- conformancePackage.ts: top-level package schema with scenario
  manifests, replay plans, shim plans, expected evidence, and
  verifier contracts. Deterministic package ID computed from
  canonical JSON. Cross-reference validation ensures internal
  consistency.
- conformanceTrustGate.ts: trust gates that compare captured runs
  against committed conformance packages (#337). Classifies semantic
  diffs from timing/normalization noise. Unknown/truncated evidence
  is never treated as equivalence.
- conformanceReplay.ts: CI replay engine that runs each scenario,
  evaluates trust gates, and detects drift between captured and
  committed evidence (#338).

Closes #336, #337, #338
2026-07-28 05:40:17 +00:00
morlutoandGitHub 645668b70d Merge pull request #443 from morluto/codex/provider-catalog-digest
fix(catalog): digest provider projections
2026-07-27 00:35:42 +08:00
morlutoandGitHub ba3151891e Merge branch 'main' into codex/provider-catalog-digest 2026-07-27 00:12:31 +08:00
morluto c4cd0330e0 fix(catalog): digest provider projections 2026-07-26 16:07:12 +00:00
morlutoandGitHub 1caa0d6e54 Merge pull request #438 from morluto/release-please--branches--main--components--rea-agents
chore(main): release rea-agents 2.6.0
2026-07-27 00:00:35 +08:00
github-actions[bot] 17c8b4c2f1 chore: normalize release product catalog 2026-07-26 15:32:38 +00:00
github-actions[bot]andGitHub 58c749e831 chore(main): release rea-agents 2.6.0 2026-07-26 15:31:39 +00:00
morlutoandGitHub 8ec9bb1bfe Merge pull request #442 from morluto/fix/mcp-cold-start
perf(mcp): reduce cold-start work
2026-07-26 23:31:04 +08:00
morluto 7157ab0b70 test(mcp): stabilize lazy-loading verification 2026-07-26 15:26:41 +00:00
morluto fecddc0549 perf(mcp): reduce cold-start work 2026-07-26 15:02:58 +00:00
morlutoandGitHub 6d37efe8fa Merge pull request #440 from morluto/fix/npx-setup-shadowing
fix(setup): refresh stale local npx bootstrap
2026-07-26 15:00:38 +08:00
morluto 90bb1610f5 fix(setup): refresh stale local npx bootstrap 2026-07-26 06:39:28 +00:00
morlutoandGitHub 1a0931b630 Merge pull request #437 from morluto/codex/complete-roadmap-workflows
feat: complete evidence-backed roadmap workflows
2026-07-25 23:30:11 +08:00
morluto 40bf55b74c test(hopper): remove brittle diagnostic source assertion 2026-07-25 15:23:56 +00:00
morlutoandGitHub b2f7561bdd Merge branch 'main' into codex/complete-roadmap-workflows 2026-07-25 23:15:53 +08:00
morluto f2c4edd2bf chore: refresh generated catalog evidence 2026-07-25 15:13:03 +00:00
morluto 4716fa2ece test: add critical provider workflow coverage 2026-07-25 15:12:51 +00:00
morluto 521139d4a5 feat(native): inspect API boundaries with evidence 2026-07-25 14:29:28 +00:00
morluto 5184816199 test(investigation): prove multi-version fixture closure 2026-07-25 14:29:15 +00:00
morluto 9522adf19a test(package): prove artifact cleanup and identity 2026-07-25 14:29:08 +00:00
morluto 7a43420029 feat(hopper): report bridge progress diagnostics 2026-07-25 14:29:02 +00:00
morlutoandGitHub 7856d460e9 Merge pull request #435 from morluto/release-please--branches--main--components--rea-agents
chore(main): release rea-agents 2.5.1
2026-07-25 17:59:10 +08:00
github-actions[bot] a17c7ef27d chore: normalize release product catalog 2026-07-25 09:18:35 +00:00
github-actions[bot]andGitHub 0190bcbef1 chore(main): release rea-agents 2.5.1 2026-07-25 09:17:41 +00:00
morlutoandGitHub fbe442b1e2 Merge pull request #436 from morluto/codex/performance-hot-paths
perf: eliminate repeated work in analysis hot paths
2026-07-25 17:17:22 +08:00
morluto 305e72995b perf(browser): lazy-load Playwright sessions 2026-07-25 09:12:17 +00:00
morluto c5e8871bda perf(dotnet): reuse authenticated artifact snapshots 2026-07-25 09:11:52 +00:00
morluto 809dfbfe48 perf(evidence): validate ledger mutations incrementally 2026-07-25 09:11:07 +00:00
morluto adc4852f3a perf(javascript): reuse parsed source across analyses 2026-07-25 09:10:51 +00:00
morluto 674364443d perf(server): cache advertised JSON schemas 2026-07-25 09:10:32 +00:00
morlutoandGitHub d43a461958 Merge pull request #434 from morluto/codex/normalize-release-pr-metadata
fix(ci): normalize generated release metadata
2026-07-25 00:52:22 +08:00
morluto 119954c701 fix(ci): normalize release pull request metadata 2026-07-24 16:44:51 +00:00
720 changed files with 52524 additions and 30032 deletions
+66 -17
View File
@@ -88,7 +88,7 @@ jobs:
strategy:
fail-fast: false
matrix:
shard: [1/2, 2/2]
shard: [1/4, 2/4, 3/4, 4/4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@@ -160,16 +160,61 @@ jobs:
path: .vitest-reports
pattern: vitest-report-*
merge-multiple: true
- if: needs.changes.outputs.code == 'true' && success()
run: npm run test:ci:merge
- name: Merge coverage, JUnit, and timing reports
if: needs.changes.outputs.code == 'true' && success()
run: >-
npm run test:ci:merge --
--coverage.reportsDirectory=coverage
--coverage.reporter=text
--coverage.reporter=json-summary
--coverage.reporter=lcov
--reporter=default
--reporter=junit
--reporter=json
--outputFile.junit=test-results.xml
--outputFile.json=test-results.json
- name: Summarize slowest test files
if: needs.changes.outputs.code == 'true' && success()
shell: bash
run: |
node <<'NODE' >> "${GITHUB_STEP_SUMMARY}"
const report = JSON.parse(require("node:fs").readFileSync("test-results.json", "utf8"));
const files = (report.testResults ?? [])
.map((result) => ({
name: result.name,
duration: Math.max(0, (result.endTime ?? result.startTime) - result.startTime),
}))
.sort((left, right) => right.duration - left.duration)
.slice(0, 20);
console.log("## Slowest deterministic test files\n");
console.log("| File | Duration (ms) |");
console.log("| --- | ---: |");
for (const file of files) {
console.log(`| \`${file.name}\` | ${file.duration} |`);
}
NODE
- uses: actions/upload-artifact@v4
if: needs.changes.outputs.code == 'true' && always()
with:
name: test-results
path: test-results.xml
path: |
test-results.xml
test-results.json
coverage/
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
@@ -234,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
@@ -255,20 +300,24 @@ 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/binaryTarget.test.ts
tests/windowsCapabilities.test.ts
tests/ghidraWindowsFixture.test.ts
tests/ghidraInstallation.test.ts
tests/ghidraProvider.test.ts
tests/ghidraTransport.test.ts
tests/ghidraTargetSnapshot.test.ts
tests/ghidraClient.test.ts
tests/ghidraLauncher.test.ts
tests/processOwnership.test.ts
tests/realGhidraWindowsWorkflow.test.ts
tests/composition/analysis-sessions/binaryTarget.test.ts
tests/conformance/providers/ghidra/windowsCapabilities.test.ts
tests/conformance/providers/ghidra/ghidraWindowsFixture.test.ts
tests/boundary/providers/ghidra/ghidraInstallation.test.ts
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/ghidraLauncher.test.ts
tests/boundary/process/processOwnership.test.ts
tests/boundary/process/processOwnershipPart2.test.ts
tests/boundary/process/processOwnershipPart3.test.ts
tests/boundary/process/processOwnershipValidation.test.ts
tests/boundary/process/processOwnershipValidationPart2.test.ts
tests/boundary/process/processOwnershipValidationPart3.test.ts
tests/conformance/providers/ghidra/realGhidraWindowsWorkflow.test.ts
- run: npm run verify:package:windows
- uses: actions/upload-artifact@v4
if: always()
+110 -5
View File
@@ -7,15 +7,17 @@ on:
env:
HUSKY: "0"
permissions:
contents: write
pull-requests: write
id-token: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
release-please:
name: Release Please
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
@@ -23,13 +25,78 @@ jobs:
- uses: googleapis/release-please-action@v4
id: release
with:
token: ${{ secrets.GITHUB_TOKEN }}
# A dedicated credential lets release-PR updates trigger CI. GitHub
# requires manual approval for pull_request events from GITHUB_TOKEN.
token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
- name: Check out release pull request
if: steps.release.outputs.prs_created == 'true'
uses: actions/checkout@v6
with:
ref: ${{ fromJSON(steps.release.outputs.pr).headBranchName }}
persist-credentials: false
- name: Set up Node.js for generated documentation
if: steps.release.outputs.prs_created == 'true'
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
package-manager-cache: false
- name: Install dependencies
if: steps.release.outputs.prs_created == 'true'
run: npm ci
- name: Regenerate release documentation
if: steps.release.outputs.prs_created == 'true'
run: npm run docs:generate
- name: Commit canonical release catalog
if: steps.release.outputs.prs_created == 'true'
shell: bash
env:
# Use the same identity for the follow-up commit so it also triggers CI.
GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }}
RELEASE_PR_JSON: ${{ steps.release.outputs.pr }}
run: |
release_pr_branch="$(node --input-type=module -e '
const value = JSON.parse(process.env.RELEASE_PR_JSON ?? "");
if (
typeof value !== "object" ||
value === null ||
typeof value.headBranchName !== "string" ||
value.headBranchName.length === 0
) {
process.exit(1);
}
process.stdout.write(value.headBranchName);
')"
changed_paths="$(git diff --name-only)"
if [[ -z "${changed_paths}" ]]; then
echo "Release documentation is already canonical."
exit 0
fi
if [[ "${changed_paths}" != "docs/product-catalog.json" ]]; then
echo "docs:generate changed unexpected tracked paths:" >&2
printf '%s\n' "${changed_paths}" >&2
exit 1
fi
git diff --check
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -- docs/product-catalog.json
git commit -m "chore: normalize release product catalog"
gh auth setup-git
git push origin "HEAD:${release_pr_branch}"
publish:
name: Publish to npm
needs: release-please
if: needs.release-please.outputs.release_created == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
@@ -68,3 +135,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
+77 -3
View File
@@ -18,10 +18,84 @@
},
"overrides": [
{
"files": ["tests/**/*.ts"],
"files": ["tests/**/*.ts", "src/**/*.test.ts"],
"rules": {
"max-lines-per-function": "off",
"max-lines": "off"
"max-lines": [
"error",
{ "max": 500, "skipBlankLines": true, "skipComments": true }
],
"max-lines-per-function": [
"error",
{ "max": 150, "skipBlankLines": true, "skipComments": true }
]
}
},
{
"files": ["src/domain/**/*.test.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": [
"**/application/**",
"**/server/**",
"**/process/**",
"**/browser/**",
"**/ghidra/**",
"**/hopper/**",
"**/replay/**",
"**/cli.*",
"**/main.*"
],
"message": "Domain tests may import only their owning domain and inward dependencies."
}
]
}
]
}
},
{
"files": ["src/application/**/*.test.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": ["**/server/**", "**/cli.*", "**/main.*"],
"message": "Service tests must use application ports rather than CLI or MCP entrypoints."
}
]
}
]
}
},
{
"files": ["tests/composition/**/*.test.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": [
"**/server/**",
"**/cli.*",
"**/main.*",
"node:child_process",
"node:net",
"node:http",
"node:https",
"ws",
"playwright"
],
"message": "Composition tests may wire provider-neutral sessions but must not invoke CLI, MCP, process, network, or browser boundaries."
}
]
}
]
}
}
],
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "2.5.0"
".": "3.1.0"
}
+9
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.
+164
View File
@@ -1,5 +1,169 @@
# Changelog
## [3.1.0](https://github.com/morluto/rea/compare/rea-agents-3.0.0...rea-agents-3.1.0) (2026-08-09)
### Features
* **electron:** add autonomous Electron runtime analysis ([#464](https://github.com/morluto/rea/issues/464)) ([8f229ed](https://github.com/morluto/rea/commit/8f229ede70c0a4e224c1cf71d61487e17b779d6a))
### Bug Fixes
* **ci:** guard Release Please PR branch parsing ([84cc819](https://github.com/morluto/rea/commit/84cc819284b9ce7ee648850c85d5daf3a33e5a24))
* **ci:** guard release PR branch parsing ([6838b6b](https://github.com/morluto/rea/commit/6838b6bff9ab5c400432caefd8c8737b86c41f07))
* harden evidence lifecycle and MCP contracts ([5155924](https://github.com/morluto/rea/commit/51559245d2e1c2d7550cfe489716950fe6154bd0))
* harden structured configuration comparisons ([#467](https://github.com/morluto/rea/issues/467)) ([6901edc](https://github.com/morluto/rea/commit/6901edc05d56063bdbe8be96511b1b21863630b5))
* **npm:** preserve explicit setup versions ([#465](https://github.com/morluto/rea/issues/465)) ([1783bf6](https://github.com/morluto/rea/commit/1783bf6dfdd4ec11e824870ce5b1a2f5a321450e))
### Code Refactoring
* **mcp:** make analysis states unrepresentable ([#468](https://github.com/morluto/rea/issues/468)) ([b8a9da0](https://github.com/morluto/rea/commit/b8a9da0c0f9f0b2b022a350d2c7c671d1fa3f886))
### Documentation
* clarify agent reverse-engineering promise ([ed4a628](https://github.com/morluto/rea/commit/ed4a62871f7924948e3774ae6cf880514a560ff4))
* clarify the agent reverse-engineering promise ([#466](https://github.com/morluto/rea/issues/466)) ([ed4a628](https://github.com/morluto/rea/commit/ed4a62871f7924948e3774ae6cf880514a560ff4))
* **npm:** simplify current setup command ([#469](https://github.com/morluto/rea/issues/469)) ([8b62b1a](https://github.com/morluto/rea/commit/8b62b1a71586e9409e4dd645f3d4947ef61e59b1))
## [3.0.0](https://github.com/morluto/rea/compare/rea-agents-2.7.0...rea-agents-3.0.0) (2026-08-01)
### ⚠ BREAKING CHANGES
* export_evidence_bundle now requires a filesystem path and no longer returns an inline bundle.
### Features
* make evidence bundles canonical MCP resources ([1712e6d](https://github.com/morluto/rea/commit/1712e6d821965ce5edc3880fe4ebab2e3cc02fd4))
### Bug Fixes
* address MCP resource review feedback ([f3b3fb0](https://github.com/morluto/rea/commit/f3b3fb07ded3d1ad72c0a4f24ca3147ef9722c5d))
* align aggregate tools with session capabilities ([ede91ff](https://github.com/morluto/rea/commit/ede91ff1f04563001f695ec3869daeb3c826154a))
* **ci:** align Vitest project worker budgets ([f47ee1a](https://github.com/morluto/rea/commit/f47ee1abb4fc8dbd5da3fa13c5870d6580b28bfa))
* **ci:** allow documented public type aliases ([97f6a0f](https://github.com/morluto/rea/commit/97f6a0f3fd2ebf8fa884d7f7ba6e1c66ecfe80e0))
* **ci:** make coverage report merging composable ([788f405](https://github.com/morluto/rea/commit/788f4057aee65dfae441249d2c9789bb996912a7))
* **ci:** stabilize Vitest project routing and shards ([f07acde](https://github.com/morluto/rea/commit/f07acde513d4c74d027b66e143cf5072246b412b))
* **errors:** preserve actionable analysis diagnostics ([0f96bf3](https://github.com/morluto/rea/commit/0f96bf365c330a7eb121a173bfa140cc2b652764))
* **mcp:** clarify routing and advertise input examples ([969519c](https://github.com/morluto/rea/commit/969519c00f31b5bc0f23dcffe8867e091461fda6))
* **mcp:** clarify target routing and caller diagnostics ([3e1e8cd](https://github.com/morluto/rea/commit/3e1e8cd3f829e6d3ac8b3c63d979f9db84dec826))
* **test:** bound boundary worker pressure ([935721d](https://github.com/morluto/rea/commit/935721df10d25d23621dbc2be90d334644e0858c))
* **test:** honor boundary ownership and public aliases ([92a1c33](https://github.com/morluto/rea/commit/92a1c33625ff88d81581554687e7708dd3c91e2d))
* **test:** retain subprocess coverage and cleanup ([7f46bb1](https://github.com/morluto/rea/commit/7f46bb18241bf3815e5e01816277d0cafe8f923b))
* **test:** retain subprocess coverage and cleanup ([dae2b9c](https://github.com/morluto/rea/commit/dae2b9cc4e8753827702ada6e51f2ee01811cfed))
* **test:** stabilize boundary timing ([f0d8cda](https://github.com/morluto/rea/commit/f0d8cdafa86bafdc6ad5d8c2d23a97ef583b53d5))
* **verify:** follow dynamic Hopper tool availability ([281e41f](https://github.com/morluto/rea/commit/281e41f7660066addcc7a204128fb1f49bf7a674))
### Code Refactoring
* preserve provider-neutral runtime seams ([a8161f0](https://github.com/morluto/rea/commit/a8161f0e5540f3261a08015dfe56a4254980c064))
### Documentation
* document resource snapshot workflow ([2a45d8d](https://github.com/morluto/rea/commit/2a45d8de7d52d0a234672d03c7dae794859f8ced))
* refresh generated SDK metadata ([c824db1](https://github.com/morluto/rea/commit/c824db1ab7aed2886e9050c1b7fe2136b77e3698))
* refresh managed completion manifest ([59d4659](https://github.com/morluto/rea/commit/59d465971ea86f933e9037fa32ac2787d935d076))
### Tests
* **contracts:** cover validation branches ([8ccdb33](https://github.com/morluto/rea/commit/8ccdb33dd25922e039140bcd1e3cbf3d5acee837))
* derive MCP SDK identity expectations ([ab4193c](https://github.com/morluto/rea/commit/ab4193c1f48020ff6baa42cb75e033110ee72b36))
* **eval:** cover navigation and address tool routing ([ba098f2](https://github.com/morluto/rea/commit/ba098f24e59efbd567ae8438f65e31af0f0b8050))
* overhaul deterministic suite architecture ([48053d1](https://github.com/morluto/rea/commit/48053d1032324cce6e2fe782ecba43d1b91a1bba))
* overhaul deterministic suite architecture ([513d9b1](https://github.com/morluto/rea/commit/513d9b15558f9174a8e2c0ab00e31cbc05dbaba7))
* **package:** verify npx upgrade on published releases ([7b6f7be](https://github.com/morluto/rea/commit/7b6f7becd257170cebf80f4f9f9c385b0000f074))
* **package:** verify npx upgrade on published releases ([1f8ad7b](https://github.com/morluto/rea/commit/1f8ad7bc68f50d396715116467f19524da7e05e6))
### Continuous Integration
* document deterministic suite ownership ([3c42403](https://github.com/morluto/rea/commit/3c42403b78b53e7b5358c2bca9bc156ef2c32efd))
## [2.7.0](https://github.com/morluto/rea/compare/rea-agents-2.6.0...rea-agents-2.7.0) (2026-07-28)
### Features
* **bytecode:** add JVM and Python bytecode providers ([1a75f8d](https://github.com/morluto/rea/commit/1a75f8dc236d8269a5fa07e042471ac79e79d728))
* **bytecode:** add JVM and Python bytecode providers ([4e0127d](https://github.com/morluto/rea/commit/4e0127d620377e5dc7e09444f9db0977e3d47ed1)), closes [#367](https://github.com/morluto/rea/issues/367)
* **conformance:** add portable conformance export package format ([91fb87d](https://github.com/morluto/rea/commit/91fb87d17e7885439762667c96167af79a2e044e))
* **conformance:** add portable conformance export package format ([4ee30e4](https://github.com/morluto/rea/commit/4ee30e4145db9b496158cd980af05e22010856a7))
* **metadata:** add deeper ObjC/Swift metadata and database save/licensing ([4602efd](https://github.com/morluto/rea/commit/4602efd27cbf8701c2dc15542042bc9d9ef76e7f))
* **metadata:** add deeper ObjC/Swift metadata and database save/licensing ([d7a0753](https://github.com/morluto/rea/commit/d7a07535b0fb61ee8f9ae648bb673dda23290653)), closes [#353](https://github.com/morluto/rea/issues/353)
* **mobile:** add Android APK/AAB/DEX and iOS IPA static investigation providers ([18624df](https://github.com/morluto/rea/commit/18624df5724a2c4d14a4e8e178660456424b2344))
* **mobile:** add Android APK/AAB/DEX and iOS IPA static investigation providers ([fdeeb27](https://github.com/morluto/rea/commit/fdeeb27f23041c19b5857fe7873106f0cd9c6244)), closes [#366](https://github.com/morluto/rea/issues/366)
* **package:** add MSIX/MSI/AppX package, resource, and signature analysis ([0ef1b91](https://github.com/morluto/rea/commit/0ef1b91cf31f6ca53991ee5ca60b4a22f430c01e))
* **package:** add MSIX/MSI/AppX package, resource, and signature analysis ([6c03f83](https://github.com/morluto/rea/commit/6c03f83b1bd55095d136aeb3700733383365ecc4)), closes [#363](https://github.com/morluto/rea/issues/363)
* **pe:** add broader PE/COFF/PDB static inspection ([f942558](https://github.com/morluto/rea/commit/f9425585736a89165390178f245b0f0f5d6cdacf))
* **pe:** add broader PE/COFF/PDB static inspection ([94e6600](https://github.com/morluto/rea/commit/94e6600faf89436e4e2689fda8050792307b268d)), closes [#362](https://github.com/morluto/rea/issues/362)
* **process:** replace procfs sampling with event-backed process-tree capture ([a5e355a](https://github.com/morluto/rea/commit/a5e355a3d52b19e89535449cd923c35e56ccc60d))
* **process:** replace procfs sampling with event-backed process-tree capture ([d5d2e56](https://github.com/morluto/rea/commit/d5d2e56d0461cb0debba540bc658a9938ac30599)), closes [#332](https://github.com/morluto/rea/issues/332)
* **protocol:** add custom TCP/UDP/IPC/XPC and auth-flow capture ([1fb22b4](https://github.com/morluto/rea/commit/1fb22b4c21e3a5581f3928003cc32ddc1cc9530b))
* **protocol:** add custom TCP/UDP/IPC/XPC and auth-flow capture ([6fa6f9b](https://github.com/morluto/rea/commit/6fa6f9b6d9595a480a590766dadcfebb8e7111b5)), closes [#365](https://github.com/morluto/rea/issues/365)
* **protocol:** add gRPC/Protobuf and JSON-RPC/MessagePack capture ([54153ed](https://github.com/morluto/rea/commit/54153ed62cb4f55d1efd4794490c2481ab34e212))
* **protocol:** add gRPC/Protobuf and JSON-RPC/MessagePack capture ([3f336fe](https://github.com/morluto/rea/commit/3f336fefb132971bc6bb37b2b59aa01a5e3be5ca)), closes [#364](https://github.com/morluto/rea/issues/364)
### Bug Fixes
* **catalog:** digest provider projections ([645668b](https://github.com/morluto/rea/commit/645668b70dfdff74582d4ac26cda417947231764))
* **catalog:** digest provider projections ([c4cd033](https://github.com/morluto/rea/commit/c4cd0330e00eeb8e272abbf6ecc6ce961e72f317))
## [2.6.0](https://github.com/morluto/rea/compare/rea-agents-2.5.1...rea-agents-2.6.0) (2026-07-26)
### Features
* complete evidence-backed roadmap workflows ([1a0931b](https://github.com/morluto/rea/commit/1a0931b630587701d495d1634fbafe3638ad5516))
* **hopper:** report bridge progress diagnostics ([7a43420](https://github.com/morluto/rea/commit/7a434200297df85c62c02bb103cf870d14f202f7))
* **native:** inspect API boundaries with evidence ([521139d](https://github.com/morluto/rea/commit/521139d4a57a8ab2e25bb262d2a1fd09fd06be92))
### Bug Fixes
* **setup:** refresh stale local npx bootstrap ([6d37efe](https://github.com/morluto/rea/commit/6d37efe8fa1fb9b09c8eaa3a1ed6b6f63188ae22))
* **setup:** refresh stale local npx bootstrap ([90bb161](https://github.com/morluto/rea/commit/90bb1610f56448846adf85b31d9ae96be12dfb4a))
### Performance Improvements
* **mcp:** reduce cold-start work ([8ec9bb1](https://github.com/morluto/rea/commit/8ec9bb1bfea8bc57441bec72f3fcfbe93cf5d4f4))
* **mcp:** reduce cold-start work ([fecddc0](https://github.com/morluto/rea/commit/fecddc0549a31e99a5ea72cbda84e448b1a961d5))
### Tests
* add critical provider workflow coverage ([4716fa2](https://github.com/morluto/rea/commit/4716fa2ece954f5e53bdda9d1f33afad57d2e151))
* **hopper:** remove brittle diagnostic source assertion ([40bf55b](https://github.com/morluto/rea/commit/40bf55b74ca6b2c90b3243a29018cb75879ba081))
* **investigation:** prove multi-version fixture closure ([5184816](https://github.com/morluto/rea/commit/5184816199895ceff6a489810e12fba0c2c41eb7))
* **mcp:** stabilize lazy-loading verification ([7157ab0](https://github.com/morluto/rea/commit/7157ab0b70833cc8237271e8625ccb7965c318c3))
* **package:** prove artifact cleanup and identity ([9522adf](https://github.com/morluto/rea/commit/9522adf19ac915247e2c74b18d3ef5cab95befc5))
## [2.5.1](https://github.com/morluto/rea/compare/rea-agents-2.5.0...rea-agents-2.5.1) (2026-07-25)
### Bug Fixes
* **ci:** normalize generated release metadata ([d43a461](https://github.com/morluto/rea/commit/d43a461958c7bef56a61f75d7f7da73b43e7c387))
* **ci:** normalize release pull request metadata ([119954c](https://github.com/morluto/rea/commit/119954c701533491ccbc4de649592e059f3f5d39))
### Performance Improvements
* **browser:** lazy-load Playwright sessions ([305e729](https://github.com/morluto/rea/commit/305e72995bd52caee7ad8b42aae531b8924e7d0d))
* **dotnet:** reuse authenticated artifact snapshots ([c5e8871](https://github.com/morluto/rea/commit/c5e8871bda737c52d6b09303f9c95e225bf845f2))
* eliminate repeated work in analysis hot paths ([fbe442b](https://github.com/morluto/rea/commit/fbe442b1e2395b93d1bffa2c192ea5ac9a0fab34))
* **evidence:** validate ledger mutations incrementally ([809dfbf](https://github.com/morluto/rea/commit/809dfbfe4868478bf62be40e5f9db35d1b52f6be))
* **javascript:** reuse parsed source across analyses ([adc4852](https://github.com/morluto/rea/commit/adc4852f3aea5bd2847ce9efb53fe43ad030dde6))
* **server:** cache advertised JSON schemas ([6743644](https://github.com/morluto/rea/commit/674364443dc12b89fe6444fdfb364f1b53086f90))
## [2.5.0](https://github.com/morluto/rea/compare/rea-agents-2.4.0...rea-agents-2.5.0) (2026-07-24)
+7 -9
View File
@@ -38,15 +38,13 @@ provider and replay execution remains uncached, but their deterministic build
prerequisite uses Turbo. CI uploads the rendered TypeDoc site as an `api-docs`
artifact; the generated HTML is not committed.
Local `npm test` runs without coverage, retries, or verbose output. Use `npm run
test:fast` for pure and subprocess groups, `npm run test:integration` for serial
filesystem/process/CLI cases. `npm run test:changed` runs changed and related
non-serial tests once; `npm run test:watch` keeps that selection live. Pure tests
use Vitest threads, while subprocess tests stay in isolated forks. CI splits the
complete suite across two native Vitest
shards, then merges their coverage and JUnit reports. `npm run test:ci` runs the
equivalent unsharded gate locally. Coverage thresholds remain in
`vitest.config.ts`.
Local `npm test` runs every deterministic Vitest project without coverage,
retries, or verbose output. Focused boundary, acceptance, changed-test, and
watch commands are documented in [docs/testing.md](docs/testing.md), together
with the path-based behavioral-depth rules. Changed-test selection follows the
import graph and is feedback, not the release gate. CI splits the complete suite
across four native Vitest shards, then merges coverage, JUnit, and JSON timing
reports. Coverage thresholds remain in `vitest.config.ts`.
CI installs dependencies once for all static checks, cancels superseded PR
runs, and skips package, Windows, and full test lanes for documentation-only
+20 -12
View File
@@ -4,7 +4,7 @@
# REA: Reverse Engineer Anything
### One CLI and MCP server for agents to reverse engineer anything
### Reverse engineer anything with agents, from app behavior down to native binaries.
**See a feature you like. Understand how it works, down to the binary level.**
@@ -89,13 +89,13 @@ REA shows how it reached its conclusions. It does not claim to recover original
### Run setup — recommended
```bash
npx rea-agents setup
npx --yes rea-agents@latest setup
```
The npm package-runner prompt, when shown, approves downloading REA for this
invocation; it does not approve any setup changes. The REA wizard separately
shows its complete plan and asks before applying it. Setup does not update
Homebrew, Node.js, or npm. `npx rea-agents setup` opens with the work it
Homebrew, Node.js, or npm. The setup command opens with the work it
enables: investigate local apps from an agent, recover evidence through a
deep-analysis provider, and reuse REA's guided workflow. It summarizes the
detected agents, then asks which capabilities to set up: agent integration
@@ -103,6 +103,11 @@ detected agents, then asks which capabilities to set up: agent integration
Nothing is preselected. Choosing agent integration opens a second empty
checklist for the specific detected agents that should receive a registration.
`@latest` makes the requested release explicit and asks npm for the release
currently published under that tag. REA does not silently replace the package
version npm selected. Intentional rollbacks therefore remain available through
an exact package request.
REA keeps the journey inline so its history remains in the terminal. Selecting
a capability does not select every detected target or authorize a change.
Before anything changes, REA validates existing configuration, prints exact
@@ -132,7 +137,7 @@ Pass installer options after `bash -s --`, for example `--dry-run`, `--no-setup`
### With an agent — recommended
```bash
npx rea-agents setup
npx --yes rea-agents@latest setup
```
Choose Agent Integration in the reviewed setup plan. REA installs the pinned MCP
@@ -144,7 +149,7 @@ Review the setup plan, approve it if appropriate, then describe the app or featu
### From Terminal — no installation
```bash
npx rea-agents setup
npx --yes rea-agents@latest setup
npx -y rea-agents@latest doctor
npx -y rea-agents@latest analyze /Applications/Notes.app
```
@@ -374,15 +379,15 @@ REA handles the app analysis in steps 1–5. The agent performs step 6 with its
| Tool family | Count | Examples |
| ------------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Native inspection | 36 | procedures, pseudocode, assembly, strings, names, segments, callers, callees, xrefs, annotations, bounded byte reads, file-offset translation |
| Investigation workflows | 12 | `binary_overview`, `analyze_function`, `batch_decompile`, `trace_feature`, exact string-to-code lookup, bounded call paths, call graphs, Swift and Objective-C discovery |
| Investigation workflows | 13 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `trace_feature`, exact string-to-code lookup, bounded call paths, call graphs, Swift and Objective-C discovery |
| Native macOS utilities | 5 | Mach-O metadata, code signatures, plists, architectures, Swift demangling; Hopper-free and provenance-bearing |
| 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 | 19 | target lifecycle, Evidence v2 bundles, direct finite replay-machine evaluation, process/artifact/function comparison, evidence-linked residual-unknown lifecycle |
| 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 |
The public interface describes what the agent is trying to learn. Providers decide how to answer. macOS utilities handle common semantic inspection without launching Hopper; Hopper handles deeper native analysis; the process harness implements controlled behavioral capture.
@@ -502,14 +507,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.
@@ -530,7 +535,7 @@ Setup detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf,
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@2.5.0", "mcp"]
"args": ["-y", "rea-agents@3.1.0", "mcp"]
}
}
}
@@ -787,7 +792,10 @@ Any agent that can run a local MCP server can use the manual configuration. Setu
## Development
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, architecture, tests, and release instructions. PR CI publishes generated API documentation as the `api-docs` workflow artifact.
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, architecture, and release
instructions, and [docs/testing.md](docs/testing.md) for the behavioral test
depths, focused developer commands, coverage floors, and CI evidence. PR CI
publishes generated API documentation as the `api-docs` workflow artifact.
`npm run verify:agent` runs brandless native, JavaScript-application, managed,
and browser prompts through a real local Codex CLI. Its JSON report measures
+6 -6
View File
@@ -65,7 +65,7 @@ npx rea-agents setup
### باستخدام وكيل برمجة — موصى به
```bash
npx rea-agents setup
npx --yes rea-agents@latest setup
```
اطلب من الوكيل إعداد REA. سيفحص جهاز Mac ويشرح ما يحتاج إلى تثبيته ويطلب الموافقة ويرشدك خلال رسائل النظام. أعد تشغيل الوكيل بعد الإعداد إذا طلب ذلك لتحميل مجموعة الأدوات الكاملة.
@@ -90,7 +90,7 @@ npx rea-agents setup
```bash
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
npx rea-agents setup
npx --yes rea-agents@latest setup
```
إذا طلب macOS أو برنامج التثبيت تأكيدًا، فأكمل الخطوة ثم شغّل الأمر نفسه مرة أخرى.
@@ -162,15 +162,15 @@ rea uninstall --purge-data
| عائلة الأدوات | العدد | أمثلة |
| --------------------- | ----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| فحص الملفات التنفيذية | 36 | الدوال، والشيفرة شبه المصدرية، والتعليمات، والسلاسل، والأسماء، والمراجع، والتعليقات |
| التحليل المركب | 12 | النظرة العامة، وفك الترجمة الدفعي، ومخططات الاستدعاء، والمراجع، واكتشاف الأنواع |
| التحليل المركب | 13 | النظرة العامة، و`inspect_native_api`، وفك الترجمة الدفعي، ومخططات الاستدعاء، والمراجع، واكتشاف الأنواع |
| أدوات macOS الأصلية | 5 | بيانات Mach-O والتوقيعات وملفات plist والمعماريات وفك رموز Swift دون تشغيل Hopper |
| رسم القطع الأثرية | 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 وتوصيف وقت التشغيل المُدار وإغلاق تغطية إعادة البناء ودفاتر التزامات حتمية والتحقق الشامل من جاهزية إعادة البناء |
| جلسة الملف التنفيذي | 19 | فتح الأهداف وحزم الأدلة وتشغيل آلة إعادة محدودة ومقارنة العمليات والقطع والدوال وسجل المجهولات المتبقية |
| جلسة الملف التنفيذي | 23 | فتح الأهداف وحزم الأدلة وتحرير الحزم المحتفظ بها وتشغيل آلة إعادة محدودة ومقارنة العمليات والقطع والدوال وسجل المجهولات المتبقية |
## استخدام REA مع وكلاء برمجة آخرين
@@ -183,7 +183,7 @@ rea uninstall --purge-data
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@2.5.0", "mcp"]
"args": ["-y", "rea-agents@3.1.0", "mcp"]
}
}
}
+6 -6
View File
@@ -67,7 +67,7 @@ REA は調査をバイナリ上の根拠に結び付けます。元のソース
### コーディングエージェントから(推奨)
```bash
npx rea-agents setup
npx --yes rea-agents@latest setup
```
エージェントに REA のセットアップを依頼してください。Mac を確認し、必要なインストールを説明して承認を求め、システムプロンプトを案内します。完全なツールを読み込むため再起動を求められた場合は、セットアップ後に再起動してください。
@@ -91,7 +91,7 @@ Ubuntu 24.04+、Fedora 41+、64 ビット Arch Linux では、REA が公式の D
```bash
# 1. REA をセットアップ
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
npx rea-agents setup
npx --yes rea-agents@latest setup
```
macOS やインストーラーから確認を求められた場合は、その操作を完了してから同じコマンドをもう一度実行してください。
@@ -152,15 +152,15 @@ REA は手順 1〜5 のバイナリ解析を処理し、手順 6 はエージェ
| ツール群 | 数 | 例 |
| ---------------------- | --: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| バイナリ調査 | 36 | プロシージャ、疑似コード、アセンブリ、文字列、名前、セグメント、callers、callees、xrefs、注釈 |
| 合成解析 | 12 | `binary_overview`, `analyze_function`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, Swift / ObjC 検出 |
| 合成解析 | 13 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, Swift / ObjC 検出 |
| macOS ネイティブ | 5 | Mach-O メタデータ、署名、plist、アーキテクチャ、Swift デマングル。Hopper 起動不要 |
| アーティファクトグラフ | 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、マネージドランタイム特性評価、再構築カバレッジ閉包、決定論的な義務台帳、エンドツーエンドの準備適合性検証 |
| バイナリセッション | 19 | `open_binary`、`binary_session`、証拠バンドル、有限リプレイ状態機械、プロセス/成果物/関数比較、残余未知レジストリ |
| バイナリセッション | 23 | `open_binary`、`binary_session`、証拠バンドル、保持バンドルの解放、有限リプレイ状態機械、プロセス/成果物/関数比較、残余未知レジストリ |
## 他のコーディングエージェントで使う
@@ -173,7 +173,7 @@ Setup は Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Windsurf
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@2.5.0", "mcp"]
"args": ["-y", "rea-agents@3.1.0", "mcp"]
}
}
}
+6 -6
View File
@@ -67,7 +67,7 @@ REA는 분석을 바이너리 증거에 근거하게 합니다. 원본 소스
### 코딩 에이전트에서 시작하기(권장)
```bash
npx rea-agents setup
npx --yes rea-agents@latest setup
```
에이전트에게 REA 설정을 요청하세요. Mac을 확인하고 필요한 설치 내용을 설명한 뒤 승인을 요청하며 시스템 프롬프트를 안내합니다. 전체 도구를 불러오기 위해 재시작하라고 하면 설정 후 에이전트를 재시작하세요.
@@ -91,7 +91,7 @@ Ubuntu 24.04+, Fedora 41+, 64비트 Arch Linux에서 REA는 공식 DEB, RPM 또
```bash
# 1. REA 설정
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
npx rea-agents setup
npx --yes rea-agents@latest setup
```
macOS나 설치 프로그램이 확인을 요청하면 해당 절차를 완료하고 같은 명령을 다시 실행하세요.
@@ -152,15 +152,15 @@ REA는 1–5단계의 바이너리 분석을 처리합니다. 6단계는 에이
| 도구 그룹 | 수 | 예시 |
| ------------------- | --: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 바이너리 검사 | 36 | 프로시저, 의사 코드, 어셈블리, 문자열, 이름, 세그먼트, callers, callees, xrefs, 주석 |
| 합성 분석 | 12 | `binary_overview`, `analyze_function`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, Swift 및 ObjC 탐색 |
| 합성 분석 | 13 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`, Swift 및 ObjC 탐색 |
| macOS 네이티브 도구 | 5 | Mach-O 메타데이터, 코드 서명, plist, 아키텍처, Swift 디맹글링. Hopper 실행 불필요 |
| 아티팩트 그래프 | 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, 관리형 런타임 특성화, 재구성 커버리지 폐쇄, 결정론적 의무 원장, 엔드투엔드 준비 적합성 검증 |
| 바이너리 세션 | 19 | `open_binary`, `binary_session`, 증거 번들, 유한 replay 상태 머신, 프로세스·아티팩트·함수 비교, 잔여 미지수 레지스트리 |
| 바이너리 세션 | 23 | `open_binary`, `binary_session`, 증거 번들, 보존 번들 해제, 유한 replay 상태 머신, 프로세스·아티팩트·함수 비교, 잔여 미지수 레지스트리 |
## 다른 코딩 에이전트에서 사용하기
@@ -173,7 +173,7 @@ Setup은 Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf, Devin
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@2.5.0", "mcp"]
"args": ["-y", "rea-agents@3.1.0", "mcp"]
}
}
}
+6 -6
View File
@@ -67,7 +67,7 @@ REA 让调查始终以二进制证据为依据。它不会声称能恢复原始
### 使用编程智能体(推荐)
```bash
npx rea-agents setup
npx --yes rea-agents@latest setup
```
让智能体设置 REA。它会检查你的 Mac,说明需要安装的内容,先征得同意,并引导你处理系统提示。如果智能体要求重启以加载完整工具,请在设置后重启。
@@ -91,7 +91,7 @@ npx rea-agents setup
```bash
# 1. 安装和配置 REA
curl -fsSL https://raw.githubusercontent.com/morluto/rea/main/install.sh | bash
npx rea-agents setup
npx --yes rea-agents@latest setup
```
如果 macOS 或安装程序要求确认,请完成提示,然后再次运行同一命令。
@@ -152,15 +152,15 @@ REA 负责第 1–5 步中的二进制分析。第 6 步由智能体使用其常
| 工具类别 | 数量 | 示例 |
| ----------------- | ---: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 二进制检查 | 36 | 过程、伪代码、汇编、字符串、名称、段、调用者、被调用者、交叉引用、注释 |
| 组合分析 | 12 | `binary_overview`, `analyze_function`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`、Swift 与 ObjC 发现 |
| 组合分析 | 13 | `binary_overview`, `analyze_function`, `inspect_native_api`, `batch_decompile`, `get_call_graph`, `find_xrefs_to_name`、Swift 与 ObjC 发现 |
| macOS 原生工具 | 5 | Mach-O 元数据、代码签名、plist、架构与 Swift 符号还原,无需启动 Hopper |
| 制品图谱 | 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、托管运行时表征、重建覆盖闭合、确定性义务账本和端到端就绪性一致性验证 |
| 二进制会话 | 19 | `open_binary`、`binary_session`、证据包、有限重放状态机、进程、制品与函数比较、残余未知项注册表 |
| 二进制会话 | 23 | `open_binary`、`binary_session`、证据包、保留包释放、有限重放状态机、进程、制品与函数比较、残余未知项注册表 |
## 与其他编程智能体一起使用
@@ -173,7 +173,7 @@ Setup 会检测 Claude Code、Claude Desktop、Codex、Cursor、Gemini CLI、Win
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@2.5.0", "mcp"]
"args": ["-y", "rea-agents@3.1.0", "mcp"]
}
}
}
+477 -4
View File
@@ -55,6 +55,7 @@ import ghidra.program.model.block.CodeBlockIterator;
import ghidra.program.model.block.CodeBlockReference;
import ghidra.program.model.block.CodeBlockReferenceIterator;
import ghidra.program.model.data.StringDataInstance;
import ghidra.program.model.data.DataType;
import ghidra.program.model.listing.CommentType;
import ghidra.program.model.listing.Data;
import ghidra.program.model.listing.DataIterator;
@@ -70,15 +71,25 @@ import ghidra.program.model.symbol.ReferenceIterator;
import ghidra.program.model.symbol.SourceType;
import ghidra.program.model.symbol.Symbol;
import ghidra.program.model.symbol.SymbolIterator;
import ghidra.program.model.pcode.FunctionPrototype;
import ghidra.program.model.pcode.HighFunction;
import ghidra.program.model.pcode.HighSymbol;
import ghidra.program.model.pcode.JumpTable;
public final class ReaGhidraBridge extends HeadlessScript {
private static final int BRIDGE_VERSION = 5;
private static final int BRIDGE_VERSION = 6;
private static final int MAX_DESCRIPTOR_BYTES = 16 * 1024;
private static final int MAX_REQUEST_CHARACTERS = 256 * 1024;
private static final int MAX_RESPONSE_BYTES = 1024 * 1024;
private static final int RESPONSE_ENVELOPE_RESERVE_BYTES = 32 * 1024;
private static final int MAX_NATIVE_API_RESPONSE_BYTES = 192 * 1024;
private static final int MAX_INVENTORY_ITEMS = 1_000_000;
private static final int MAX_FUNCTION_ITEMS = 100_000;
private static final int MAX_FUNCTION_INSTRUCTIONS = 100_000;
private static final int MAX_NATIVE_API_PARAMETERS = 64;
private static final int MAX_NATIVE_API_JUMP_TABLES = 4;
private static final int MAX_NATIVE_API_LOAD_TABLES = 4;
private static final int MAX_NATIVE_API_MAPPINGS = 32;
private static final int DECOMPILE_TIMEOUT_SECONDS = 30;
private static final int DECOMPILE_PAYLOAD_MBYTES = 8;
private static final int MAX_LIST_VALUE_CODE_POINTS = 1_024;
@@ -377,7 +388,9 @@ public final class ReaGhidraBridge extends HeadlessScript {
private JsonElement procedurePseudocode(JsonObject params) throws Exception {
requireKeys(params, Set.of("document", "procedure"));
requireDocument(params);
String value = decompile(resolveProcedure(requireString(params, "procedure")));
String value = decompiledText(
decompile(resolveProcedure(requireString(params, "procedure")))
);
return value == null ? JsonNull.INSTANCE : GSON.toJsonTree(value);
}
@@ -576,7 +589,8 @@ public final class ReaGhidraBridge extends HeadlessScript {
);
JsonObject offsets = requireCollectionOffsets(params);
InstructionScan scan = scanInstructions(function, maximumInstructions);
String pseudocode = decompile(function);
DecompileResults decompilation = decompile(function);
String pseudocode = decompiledText(decompilation);
if (pseudocode == null) {
pseudocode = "";
}
@@ -662,6 +676,19 @@ public final class ReaGhidraBridge extends HeadlessScript {
instructionScan.addProperty("scanned", scan.instructions.size());
instructionScan.addProperty("truncated", scan.truncated);
result.add("instruction_scan", instructionScan);
int nativeApiBudget = Math.min(
MAX_NATIVE_API_RESPONSE_BYTES,
Math.max(
0,
MAX_RESPONSE_BYTES -
encodedUtf8Length(result) -
RESPONSE_ENVELOPE_RESERVE_BYTES
)
);
result.add(
"native_api",
nativeApiBoundary(function, decompilation, nativeApiBudget)
);
JsonArray limitations = new JsonArray();
limitations.add(
"Unresolved computed or indirect flows without target addresses are not represented as reference edges."
@@ -971,7 +998,446 @@ public final class ReaGhidraBridge extends HeadlessScript {
return matches.get(0);
}
private String decompile(Function function) {
private JsonObject nativeApiBoundary(
Function function,
DecompileResults decompilation,
int maximumBytes
) throws Exception {
if (decompilation == null || decompilation.getHighFunction() == null) {
return unavailableNativeApiBoundary(
"Ghidra has no HighFunction model for an external or bodyless procedure"
);
}
HighFunction highFunction = decompilation.getHighFunction();
FunctionPrototype prototype = highFunction.getFunctionPrototype();
String signatureSource = function
.getSignatureSource()
.name()
.toLowerCase(Locale.ROOT);
JsonObject result = new JsonObject();
result.addProperty("available", true);
result.addProperty("provenance", "ghidra-high-function");
result.addProperty("signature_source", signatureSource);
String callingConvention = prototype.getModelName();
result.addProperty(
"calling_convention",
callingConvention == null || callingConvention.isBlank()
? "unknown"
: callingConvention
);
result.add(
"return_type",
inferredBoundaryType(
"return",
null,
null,
prototype.getReturnType(),
prototype.getReturnStorage().toString(),
false,
signatureSource
)
);
JsonArray parameters = new JsonArray();
result.add("parameters", parameters);
result.addProperty("parameters_truncated", false);
JsonArray jumpTables = new JsonArray();
result.add("jump_tables", jumpTables);
result.addProperty("jump_tables_truncated", false);
JsonObject pseudocode = new JsonObject();
pseudocode.addProperty("classification", "decompiler-generated-non-source");
pseudocode.addProperty("compilable", false);
result.add("pseudocode", pseudocode);
JsonArray artifacts = new JsonArray();
artifacts.add("recovered-signature");
artifacts.add("register-and-stack-variables");
artifacts.add("compiler-generated-control-flow");
artifacts.add("pointer-arithmetic");
artifacts.add("pseudocode");
result.add("decompiler_artifacts", artifacts);
JsonArray limitations = new JsonArray();
limitations.add(
"Types and calling conventions are Ghidra decompiler observations, not source declarations."
);
limitations.add(
"Register variables, compiler-generated blocks, and pointer arithmetic remain decompiler artifacts."
);
limitations.add(
"Pseudocode is neither original source nor guaranteed to compile."
);
result.add("limitations", limitations);
if (encodedUtf8Length(result) > maximumBytes) {
return unavailableNativeApiBoundary(
"The remaining Ghidra response budget cannot contain a structured native API boundary"
);
}
int parameterCount = Math.min(
prototype.getNumParams(),
MAX_NATIVE_API_PARAMETERS
);
for (int index = 0; index < parameterCount; index += 1) {
HighSymbol parameter = prototype.getParam(index);
if (parameter == null) {
continue;
}
parameters.add(
inferredBoundaryType(
"parameter",
index,
parameter.getName(),
parameter.getDataType(),
parameter.getStorage().toString(),
parameter.isTypeLocked(),
signatureSource
)
);
if (encodedUtf8Length(result) > maximumBytes) {
parameters.remove(parameters.size() - 1);
break;
}
}
result.addProperty(
"parameters_truncated",
prototype.getNumParams() > parameters.size()
);
JumpTable[] recoveredJumpTables = highFunction.getJumpTables();
int jumpTableCount = Math.min(
recoveredJumpTables.length,
MAX_NATIVE_API_JUMP_TABLES
);
for (int index = 0; index < jumpTableCount; index += 1) {
jumpTables.add(inferredJumpTable(recoveredJumpTables[index]));
if (encodedUtf8Length(result) > maximumBytes) {
jumpTables.remove(jumpTables.size() - 1);
break;
}
}
result.addProperty(
"jump_tables_truncated",
recoveredJumpTables.length > jumpTables.size()
);
return result;
}
private static JsonObject unavailableNativeApiBoundary(String reason) {
JsonObject unavailable = new JsonObject();
unavailable.addProperty("available", false);
unavailable.addProperty("reason", reason);
JsonArray unknowns = new JsonArray();
unknowns.add("Return and parameter boundary types are unavailable.");
unknowns.add("Jump-table mappings are unavailable.");
unavailable.add("residual_unknowns", unknowns);
return unavailable;
}
private static int encodedUtf8Length(JsonElement value) {
return GSON.toJson(value).getBytes(StandardCharsets.UTF_8).length;
}
private static JsonObject inferredBoundaryType(
String role,
Integer ordinal,
String name,
DataType dataType,
String storage,
boolean typeLocked,
String signatureSource
) {
JsonObject result = new JsonObject();
result.addProperty("role", role);
if (ordinal == null) {
result.add("ordinal", JsonNull.INSTANCE);
}
else {
result.addProperty("ordinal", ordinal);
}
if (name == null || name.isBlank()) {
result.add("name", JsonNull.INSTANCE);
}
else {
result.addProperty("name", name);
}
result.addProperty("data_type", dataType.getDisplayName());
int dataTypeLength = dataType.getLength();
if (dataTypeLength < 0) {
result.add("size_bytes", JsonNull.INSTANCE);
}
else {
result.addProperty("size_bytes", dataTypeLength);
}
if (storage.isBlank()) {
result.add("storage", JsonNull.INSTANCE);
}
else {
result.addProperty("storage", storage);
}
result.addProperty(
"confidence",
inferenceConfidence(signatureSource, typeLocked)
);
JsonArray evidence = new JsonArray();
evidence.add(
inferenceEvidence(
"signature-source",
"ghidra-function-manager",
"Function signature source is " + signatureSource + "."
)
);
evidence.add(
inferenceEvidence(
"decompiler-type",
"ghidra-high-function",
"HighFunction recovered data type " + dataType.getDisplayName() + "."
)
);
if (typeLocked) {
evidence.add(
inferenceEvidence(
"type-lock",
"ghidra-high-symbol",
"The decompiler marks this parameter type as locked."
)
);
}
result.add("evidence", evidence);
JsonArray artifacts = new JsonArray();
artifacts.add("recovered-signature");
if (!storage.isBlank()) {
artifacts.add("register-or-stack-storage");
}
if (name != null && (name.startsWith("param_") || name.startsWith("unaff_"))) {
artifacts.add("compiler-generated-variable");
}
result.add("decompiler_artifacts", artifacts);
return result;
}
private JsonObject inferredJumpTable(JumpTable jumpTable) throws Exception {
JsonObject result = new JsonObject();
result.addProperty(
"dispatch_address",
canonicalAddress(jumpTable.getSwitchAddress())
);
JumpTable.LoadTable[] loadTables = jumpTable.getLoadTables();
List<Address> referencedDataAddresses = loadTables.length == 0
? dispatchPathDataReferences(jumpTable)
: List.of();
JsonArray dataSources = new JsonArray();
int loadTableCount = Math.min(
loadTables.length,
MAX_NATIVE_API_LOAD_TABLES
);
for (int index = 0; index < loadTableCount; index += 1) {
JumpTable.LoadTable loadTable = loadTables[index];
JsonObject source = new JsonObject();
source.addProperty("address", canonicalAddress(loadTable.getAddress()));
source.addProperty("provenance", "ghidra-decompiler-load-table");
source.addProperty("entry_size_bytes", loadTable.getSize());
source.addProperty("entry_count", loadTable.getNum());
source.addProperty("confidence", "high");
JsonArray evidence = new JsonArray();
evidence.add(
inferenceEvidence(
"jump-table",
"ghidra-high-function",
"Decompiler load table starts at " +
canonicalAddress(loadTable.getAddress()) +
"."
)
);
source.add("evidence", evidence);
dataSources.add(source);
}
int referencedDataCount = Math.min(
referencedDataAddresses.size(),
MAX_NATIVE_API_LOAD_TABLES - loadTableCount
);
for (int index = 0; index < referencedDataCount; index += 1) {
Address address = referencedDataAddresses.get(index);
JsonObject source = new JsonObject();
source.addProperty("address", canonicalAddress(address));
source.addProperty(
"provenance",
"ghidra-dispatch-block-data-reference"
);
source.add("entry_size_bytes", JsonNull.INSTANCE);
source.add("entry_count", JsonNull.INSTANCE);
source.addProperty("confidence", "medium");
JsonArray evidence = new JsonArray();
evidence.add(
inferenceEvidence(
"jump-table",
"ghidra-reference-manager",
"The dispatch block or an immediate predecessor contains a typed data reference to " +
canonicalAddress(address) +
"."
)
);
source.add("evidence", evidence);
dataSources.add(source);
}
result.add("data_sources", dataSources);
result.addProperty(
"data_sources_truncated",
loadTables.length > loadTableCount ||
referencedDataAddresses.size() > referencedDataCount
);
Address[] targets = jumpTable.getCases();
Integer[] labels = jumpTable.getLabelValues();
JsonArray mappings = new JsonArray();
int mappingCount = Math.min(targets.length, MAX_NATIVE_API_MAPPINGS);
for (int index = 0; index < mappingCount; index += 1) {
JsonObject mapping = new JsonObject();
if (labels == null || index >= labels.length || labels[index] == null) {
mapping.add("case_value", JsonNull.INSTANCE);
}
else {
mapping.addProperty("case_value", labels[index]);
}
String targetAddress = canonicalAddress(targets[index]);
mapping.addProperty("target_address", targetAddress);
JsonArray dataAddresses = new JsonArray();
for (int sourceIndex = 0; sourceIndex < loadTableCount; sourceIndex += 1) {
dataAddresses.add(
canonicalAddress(loadTables[sourceIndex].getAddress())
);
}
mapping.add("data_addresses", dataAddresses);
mapping.addProperty("confidence", "medium");
JsonArray evidence = new JsonArray();
evidence.add(
inferenceEvidence(
"jump-table",
"ghidra-high-function",
"Recovered target " +
targetAddress +
" from dispatch " +
canonicalAddress(jumpTable.getSwitchAddress()) +
"."
)
);
mapping.add("evidence", evidence);
mappings.add(mapping);
}
result.add("mappings", mappings);
result.addProperty("mappings_truncated", targets.length > mappingCount);
JsonArray limitations = new JsonArray();
if (loadTables.length == 0 && referencedDataAddresses.isEmpty()) {
limitations.add(
"Ghidra recovered targets but exposed no backing load-table or dispatch-block data address."
);
}
if (loadTables.length == 0 && !referencedDataAddresses.isEmpty()) {
limitations.add(
"Load-table metadata was absent; table-level candidate ownership is inferred from typed data references in the dispatch block and its immediate control-flow predecessors, but candidates are not assigned to individual mappings."
);
}
if (labels == null || labels.length != targets.length) {
limitations.add(
"Case labels were unavailable or incomplete; null labels are preserved."
);
}
if (
loadTables.length > loadTableCount ||
referencedDataAddresses.size() > referencedDataCount ||
targets.length > mappingCount
) {
limitations.add(
"Native API jump-table observations reached REA's bounded output limits."
);
}
result.add("limitations", limitations);
return result;
}
private List<Address> dispatchPathDataReferences(JumpTable jumpTable)
throws Exception {
BasicBlockModel model = new BasicBlockModel(currentProgram, false);
CodeBlock dispatchBlock = model.getFirstCodeBlockContaining(
jumpTable.getSwitchAddress(),
monitor
);
if (dispatchBlock == null) {
return List.of();
}
List<CodeBlock> blocks = new ArrayList<>();
blocks.add(dispatchBlock);
CodeBlockReferenceIterator sources = dispatchBlock.getSources(monitor);
while (sources.hasNext()) {
monitor.checkCancelled();
CodeBlock sourceBlock = sources.next().getSourceBlock();
if (sourceBlock != null) {
blocks.add(sourceBlock);
}
}
Set<Address> addresses = new HashSet<>();
for (CodeBlock block : blocks) {
InstructionIterator instructions = currentProgram
.getListing()
.getInstructions(block, true);
while (instructions.hasNext()) {
monitor.checkCancelled();
Instruction instruction = instructions.next();
for (
Reference reference : currentProgram
.getReferenceManager()
.getReferencesFrom(instruction.getAddress())
) {
if (
reference.getReferenceType().isData() &&
currentProgram
.getMemory()
.getLoadedAndInitializedAddressSet()
.contains(reference.getToAddress())
) {
addresses.add(reference.getToAddress());
}
}
}
}
List<Address> ordered = new ArrayList<>(addresses);
ordered.sort(Address::compareTo);
return List.copyOf(ordered);
}
private static JsonObject inferenceEvidence(
String kind,
String source,
String detail
) {
JsonObject evidence = new JsonObject();
evidence.addProperty("kind", kind);
evidence.addProperty("source", source);
evidence.addProperty("detail", detail);
return evidence;
}
private static String inferenceConfidence(
String signatureSource,
boolean typeLocked
) {
if (
typeLocked ||
signatureSource.equals("user_defined") ||
signatureSource.equals("imported")
) {
return "high";
}
return signatureSource.equals("analysis") ? "medium" : "low";
}
private DecompileResults decompile(Function function) {
if (function.isExternal() || function.getBody().isEmpty()) {
return null;
}
@@ -995,6 +1461,13 @@ public final class ReaGhidraBridge extends HeadlessScript {
"Ghidra decompilation failed: " + boundedMessage(results.getErrorMessage())
);
}
return results;
}
private static String decompiledText(DecompileResults results) {
if (results == null) {
return null;
}
DecompiledFunction value = results.getDecompiledFunction();
return value == null ? null : value.getC();
}
+54 -4
View File
@@ -1046,29 +1046,65 @@ def _serve_connection(connection):
if not line or len(line) > MAX_LINE_BYTES:
break
request_id = None
authenticated = False
should_stop = False
try:
request = json.loads(line.decode("utf-8"))
if set(request) != {"id", "token", "method", "params"}:
raise ValueError("Invalid bridge request shape")
request_id = request["id"]
if isinstance(request_id, bool) or not isinstance(request_id, int) or request_id < 0:
raise ValueError("Invalid bridge request id")
if not isinstance(request["token"], str) or not hmac.compare_digest(request["token"], REA_TOKEN):
raise PermissionError("Invalid bridge capability")
authenticated = True
_write_message(file, {"id": request_id, "event": {
"type": "progress",
"phase": "hopper_bridge",
"completed": 0,
"total": 1,
"message": "Hopper bridge started request",
}})
result = _dispatch(request["method"], request["params"])
should_stop = request["method"] == "shutdown_document" or (
request["method"] == "shutdown" and not result.get("cleanup_required", False)
)
response = {"id": request_id, "result": _json_safe(result)}
safe_result = _json_safe(result)
_write_message(file, {"id": request_id, "event": {
"type": "progress",
"phase": "hopper_bridge",
"completed": 1,
"total": 1,
"message": "Hopper bridge completed request",
"terminal": True,
}})
response = {"id": request_id, "result": safe_result}
except Exception as error:
response = {"id": request_id if isinstance(request_id, int) else 0, "error": {"code": -32000, "message": str(error)[:512], "type": _diagnostic_type(error)}}
file.write((json.dumps(response, separators=(",", ":")) + "\n").encode("utf-8"))
file.flush()
diagnostic = _safe_diagnostic(error)
if authenticated:
_write_message(file, {"id": request_id, "event": {
"type": "diagnostic",
"error": diagnostic,
}})
response_id = request_id if (
isinstance(request_id, int)
and not isinstance(request_id, bool)
and request_id >= 0
) else 0
response = {"id": response_id, "error": diagnostic}
_write_message(file, response)
if should_stop:
break
file.close()
connection.close()
def _write_message(file, message):
"""Write and flush one compact bridge message."""
file.write((json.dumps(message, separators=(",", ":")) + "\n").encode("utf-8"))
file.flush()
def _diagnostic_type(error):
if isinstance(error, CapabilityUnavailableError):
return "capability_unavailable"
@@ -1079,6 +1115,20 @@ def _diagnostic_type(error):
return "bridge_exception"
def _safe_diagnostic(error):
"""Project one exception without retaining provider or credential text."""
diagnostic_type = _diagnostic_type(error)
if diagnostic_type == "capability_unavailable":
message = str(error)[:512]
elif diagnostic_type == "authorization":
message = "Invalid bridge capability"
elif diagnostic_type == "invalid_request":
message = "Invalid Hopper bridge request"
else:
message = "%s: Hopper bridge operation failed" % type(error).__name__[:128]
return {"code": -32000, "message": message, "type": diagnostic_type}
def _run():
"""Own a permission-restricted, single-client Unix socket for this bridge."""
if os.path.exists(REA_SOCKET):
+52
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.
+66 -3
View File
@@ -7,7 +7,7 @@ REA separates installing its CLI from configuring external software and agents.
The recommended setup entrypoint is:
```bash
npx rea-agents setup
npx --yes rea-agents@latest setup
```
If npm asks to download and run the package, that approval applies only to the
@@ -15,12 +15,28 @@ current package-runner invocation. REA still prints its own setup plan and asks
for separate approval before changing agent configuration or installing a
product-owned component.
The explicit `@latest` request prevents a project dependency or stale npx cache
entry from choosing the setup version.
REA runs the version npm selected and never silently replaces an intentional
version request after launch. To repair registrations written by an older
release, rerun the command above and review the resulting setup plan.
For an intentional rollback, make the package request explicit:
```bash
npm exec --yes --package=rea-agents@2.4.0 -- rea setup
```
Setup continues to pin persistent MCP registrations to the exact version that
performed setup. Running current setup later migrates unversioned or older
managed registrations through the normal reviewed setup transaction.
REA supports Node.js 22.19+ and 24.11+ (including newer releases). It uses the npm already paired with that runtime and never upgrades Node.js, npm, or Homebrew.
Running `npm install rea-agents` without `--global` installs the executable only
in the current project's `node_modules/.bin`; it does not make `rea` available
on the shell `PATH`. Use `npx rea-agents setup` for the guided setup journey,
`npx -y rea-agents@latest` for unattended one-off commands, or install globally
on the shell `PATH`. Use the explicit setup command above for the guided setup
journey, `npx -y rea-agents@latest` for unattended one-off commands, or install globally
with `npm install --global rea-agents` for a shell-visible `rea` command.
The optional curl wrapper installs only the global npm package:
@@ -145,6 +161,26 @@ host mount and the rest of `/tmp` remain unchanged; this fallback never invokes
`sudo`. `rea doctor --provider hopper --detail full --json` reports the selected
strategy and both host and effective mount facts.
### Hopper in CI
REA's unattended Linux path is validated against Hopper's offered demo mode.
A paid license is not required for that path, and REA does not read, install, or
automate license credentials. Licensed Hopper installations remain supported,
but license activation is an operator-owned prerequisite rather than part of
REA setup.
macOS requires Hopper's first-run UI to be completed in the same user session
that will run REA: choose the demo mode or activate an existing license before
starting an unattended job. Ephemeral macOS runners therefore need a
pre-provisioned user session or a deliberate interactive bootstrap step.
When Hopper cannot start in CI, run
`rea doctor --provider hopper --detail full --json` in the failing runner.
Structured failures distinguish private-display dependencies, an unsupported
demo dialog or build, process-ownership conflicts, and an early lifecycle exit.
Apply the reported remediation rather than exposing the runner's desktop,
copying license secrets into logs, or killing unrelated Hopper processes.
## Ghidra
REA's Ghidra provider is bring-your-own and supports Linux x64 with the exact
@@ -221,3 +257,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.
+30 -5
View File
@@ -60,8 +60,10 @@ shutdown rules still apply; REA never kills a process it cannot prove it owns.
## Evidence resources
Evidence remains inline for compatibility and successful Evidence-producing
tools also return `resource_link` content. Session resources are connection-local:
Evidence-producing tools return compact summaries plus `resource_link` content.
Every link is accompanied by an ordinary instruction to copy its opaque URI
unchanged and call MCP `resources/read` (Codex: `read_mcp_resource`). Session
resources are connection-local:
- `rea://evidence/{evidenceId}` returns the complete immutable Evidence v2 record.
- `rea://evidence/{evidenceId}/section/{section}` returns a bounded result section.
@@ -69,9 +71,10 @@ tools also return `resource_link` content. Session resources are connection-loca
`protocol`, `nodes`, `occurrences`, and `edges` when present.
- `rea://unknown/{unknownId}` returns the current residual-unknown head and its
immutable revision history.
- `rea://unknowns/active` returns current unresolved heads.
- `rea://snapshot/{snapshotDigest}` returns the current immutable analysis
snapshot when its canonical content digest matches.
- `rea://evidence-bundle/{bundleDigest}` returns immutable canonical bytes
retained by `snapshot_evidence_bundle`.
- `rea://snapshot/current` returns the mutable native analysis cache as an
`available` or `unavailable` state.
- `rea://artifact/{manifestId}/{collection}` returns a canonical artifact
`nodes`, `occurrences`, or `edges` page with Evidence provenance.
- `rea://function/{targetSha256}/{address}` returns a retained function dossier
@@ -84,11 +87,33 @@ workspace `resource_link` blocks. Workspace resources preserve revision and
`previous_revision_digest` commitments; persistent workspace files remain
subject to configured read/write roots.
Retained bundle URIs are session-scoped and are invalidated by
`release_evidence_bundle` or session close. When retention reaches its bounded
capacity, release an unneeded digest and retry; for cross-session recovery use
`export_evidence_bundle` followed by `import_evidence_bundle` and
`snapshot_evidence_bundle` in the new session.
`export_evidence_bundle` is file-only and requires `path`; existing files
require `overwrite: true`. Use `snapshot_evidence_bundle` and then
`resources/read` when the complete bundle is needed within the current session.
An Evidence URI is discoverability, not authorization. It cannot authorize file
access, extraction, mounting, execution, or networking. IDs disappear when the
session ends unless the existing Evidence bundle or workspace persistence flow
explicitly retains them.
## Aggregate native context
`get_navigation_context` composes the selected document, current address, and
current/containing procedure. Its capability inventory exposes a
`current_selection` mode and an `explicit_document` mode; the latter works when
the caller supplies `document` and the provider lacks `current_document`. A
cursor outside a procedure is represented as
`procedure: null`. `inspect_address_context` requires an explicit address and
returns bounded name, procedure, comment, inline-comment, and bookmark facets;
unsupported facets are local `unavailable` outcomes. The scalar getters remain
available while the aggregate contracts are evaluated.
## Permission policy
All local side effects use one scope vocabulary: capability, canonical roots,
+4
View File
@@ -20,6 +20,10 @@ also requires the agent to keep observations, inferences, and unknowns
distinct. Requested prompt arguments and completion choices are rendered as
untrusted selection data, not instructions or authorization.
For target paths in `compare_application_versions`, the rendered workflow
opens each target before calling `inventory_artifact`; artifact inventory and
inspection operate on the active target and do not accept a path themselves.
Use standard MCP discovery and retrieval:
```json
+5 -3
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
+30 -15
View File
@@ -2,16 +2,16 @@
"catalog_schema_version": 1,
"package": {
"name": "rea-agents",
"version": "2.5.0",
"version": "3.1.0",
"sdk": {
"server": "2.0.0-beta.4",
"client": "2.0.0-beta.4",
"core": "2.0.0-beta.4"
"server": "2.0.0",
"client": "2.0.0",
"core": "2.0.0"
},
"skill_version": "23"
},
"tools": {
"total": 110,
"total": 116,
"families": [
{
"id": "direct",
@@ -59,7 +59,7 @@
{
"id": "enhanced",
"surface": "enhanced",
"count": 12,
"count": 13,
"tools": [
"analyze_function",
"analyze_swift_types",
@@ -70,6 +70,7 @@
"get_call_graph",
"get_objc_classes",
"get_objc_protocols",
"inspect_native_api",
"swift_classes",
"trace_call_path",
"trace_feature"
@@ -127,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"
@@ -166,7 +168,7 @@
{
"id": "session",
"surface": "session",
"count": 19,
"count": 23,
"tools": [
"binary_session",
"build_call_path",
@@ -179,11 +181,15 @@
"correlate_static_and_runtime",
"export_evidence_bundle",
"find_changed_behavior",
"get_navigation_context",
"import_evidence_bundle",
"inspect_address_context",
"list_unknowns",
"open_binary",
"record_unknown",
"release_evidence_bundle",
"run_replay_machine",
"snapshot_evidence_bundle",
"update_unknown",
"verify_reconstruction",
"verify_unknown_resolution"
@@ -364,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",
@@ -581,7 +593,7 @@
}
],
"cli": {
"primary_count": 66,
"primary_count": 68,
"commands": [
"analyze",
"analyze-javascript-application",
@@ -589,6 +601,7 @@
"build-reconstruction-obligation-ledger",
"capabilities",
"capture-browser-scenario",
"capture-electron-scenario",
"capture-process",
"capture-web-screenshot",
"commit-reconstruction-coverage",
@@ -619,6 +632,7 @@
"inspect-managed-artifact",
"inspect-managed-members",
"inspect-managed-native-boundaries",
"inspect-native-api",
"inspect-plist",
"inspect-signature",
"inspect-web-page",
@@ -659,17 +673,18 @@
},
"runtime_catalog": {
"counts": {
"cli_commands": 66,
"mcp_tools": 110,
"cli_commands": 68,
"mcp_tools": 116,
"mcp_prompts": 6,
"mcp_resources": 2,
"mcp_resource_templates": 11
},
"digests": {
"tools_sha256": "9a033ddd40fcb8273365cc895bbf75d943bf1c5c8c2ee1eb9c4b3494064aaf58",
"prompts_sha256": "f6da3ad38a1000c11364d1710f122ac4990551f04efd8c53ffb2e819b5cf5707",
"resources_sha256": "339a984ea8cd8be5f234adf24395bf0295258d990492005cf6fe1f293d12cd46",
"combined_sha256": "e8cac51f2f17a5b9a088c3ff773473c4897823401481fc0875a28fe7daf8eee0"
"tools_sha256": "b0e8b9c894ef347b6680a3a5bfdb4cba5c10e9129eeaa43a5ea22ce4d398d417",
"prompts_sha256": "3886a3bf55a448def79e3eb429e6a56eb2ba075423c5cb1e5747642b389ef8fe",
"resources_sha256": "e0e872276ebf6f88e916ef24930a99eed27ce4c93e494e60820e9bd8cc8497c3",
"combined_sha256": "bab5657d9525ec4f798d0a9ef6001412d9c11f77fb0ddc925c7076fd10921dc9",
"providers_sha256": "7446b51788734ebf6f4f702e3a5a8c9f0b61967490c245f30bd37b2060827e6b"
}
}
}
+73
View File
@@ -0,0 +1,73 @@
# Test architecture migration ledger
This ledger records how the flat test suite was classified during the
coordinated architecture cutover. It is an evidence index, not a declaration
that every test became independent merely because its path changed.
## Classification batches
The following mappings are grounded in the current moved paths. They describe
ownership only; decomposition and evidence replacement must be recorded in the
per-suite table below.
| Previous path | Current path | Classification |
| --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ----------------------------------- |
| `tests/applicationWorkflowCli.test.ts` | `tests/acceptance/applications/applicationWorkflowCli.test.ts` | Acceptance application workflow |
| `tests/setupLifecycle.test.ts` | `tests/acceptance/setup/setupLifecycle.test.ts` | Acceptance setup workflow |
| `tests/cdpBrowserProvider.test.ts` | `tests/boundary/browser/cdpBrowserProvider*.test.ts` | Browser boundary |
| `tests/ghidraClient.test.ts` | `tests/boundary/providers/ghidra/ghidraClient.test.ts` | Ghidra provider boundary |
| `tests/processOwnership.test.ts` | `tests/boundary/process/processOwnership*.test.ts` | Process boundary |
| `tests/binarySession.test.ts` | `tests/composition/analysis-sessions/binarySession.*.test.ts` | Analysis-session composition |
| `tests/javascriptSemanticAnalysis.test.ts` | `tests/composition/javascript-applications/javascriptSemanticAnalysis.*.test.ts` | JavaScript-application composition |
| `tests/config.test.ts` | `tests/composition/setup-lifecycle/config.test.ts` | Setup-lifecycle composition |
| `tests/composition/analysis-sessions/runtime.test.ts` | `tests/acceptance/applications/runtime.test.ts` | Compiled MCP acceptance workflow |
| `tests/composition/runtime-observation/webCaptureDiff.test.ts` | `tests/boundary/browser/webCaptureDiff.test.ts` | Browser boundary |
| `tests/conformanceFixtures.test.ts` | `tests/conformance/providers/conformanceFixtures.test.ts` | Provider conformance |
| `tests/codexAgentEval.test.ts` | `tests/evaluation/model-evals/codexAgentEval.test.ts` | Deterministic model-evaluator logic |
| `tests/composition/analysis-sessions/{boundedCartesianProjection,symbolAnalysis,jsonShape}.test.ts` | `src/domain/{boundedCartesianProjection,symbolAnalysis,jsonShape}.test.ts` | Colocated pure-domain behavior |
The same directory mapping applies to the other files moved in each batch:
`tests/acceptance/{analysis,applications,investigations,setup}`,
`tests/boundary/{browser,cli,filesystem,mcp,process,providers}`,
`tests/composition/{analysis-sessions,evidence-investigations,javascript-applications,process-capture,runtime-observation,setup-lifecycle}`,
`tests/conformance`, and `tests/evaluation`. Git rename detection remains the
canonical source for the complete old-to-new path list.
## Per-suite evidence record
Add a row when a legacy suite is materially decomposed, consolidated, replaced,
or removed. “Replacement evidence” must identify executable behavior, a
generated artifact comparison, a protocol probe, or an explicit verifier; a new
path alone is not replacement evidence.
| Legacy suite or claim | Disposition | Replacement evidence | Validation | Remaining gap |
| ----------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| Flat path classification | Moved | Vitest configuration contract proves every deterministic test belongs to exactly one project | Configuration contract test | Record any exceptions discovered during cutover |
| Direct session construction outside owner tests | Consolidated | `composeBinarySession` and `composeBinarySessionFromProvider` are the shared production/test seams | Repository search plus session-composition and acceptance tests | None |
| Repeated temporary-directory setup | Consolidated | `createTestTempDirectory` delegates every test-owned root to the typed workspace fixture with HOME/XDG projection and awaited cleanup | Repository search plus filesystem, process, runtime suites, and the Vitest topology hygiene contract | None; direct `mkdtemp` remains only in the workspace fixture and package-canary source text |
| Repeated CLI/MCP connection helpers | Consolidated | Typed CLI and MCP fixtures under `tests/support`; CLI output, dispatcher, and server tests use them | Acceptance startup, typed failure, lifecycle, and teardown tests | Provider-specific protocol harnesses remain local |
| Implementation-shaped assertions | Replaced | Recording adapters, executable bridge/protocol probes, generated-artifact comparisons, and supported configuration contracts | Focused boundary/conformance tests and repository search | None identified in the final audit |
| Module mocks and spies | Replaced | Recording adapters and caller-visible outcomes | Repository search plus focused reload, Ghidra, and enhanced-tool tests | None |
| `processCapture.test.ts` | Consolidated | Ten lifecycle, observation, reactive, replay, terminal, validation, and trace suites | 42 focused boundary tests | None |
| `cdpBrowserProvider.test.ts` | Consolidated | Five discovery, document/script, network, lifecycle, and sensitive-data suites | 36 focused browser-boundary tests | None |
| `javascriptSemanticAnalysis.test.ts` | Consolidated | Six structure, call, dataflow, runtime, native-binding, and rejection suites | 29 focused composition tests | None |
| `binarySession.test.ts` | Consolidated | Nine provider binding, lifecycle, cache, Evidence, permissions, and shutdown suites | Focused composition tests and shared composition-seam test | None |
| MCP mega-suites | Consolidated | Tool/resource-family suites plus catalog-wide identity and lifecycle coverage | Focused MCP tests and complete deterministic run | None |
| Provider client mega-suites | Consolidated | Capability-named protocol, timeout, ownership, error, and cleanup suites | 85 focused provider tests | Real systems remain in `verify:*` lanes |
| Pure domain proofs | Moved | Colocated bounded projection, symbol classification, and JSON-shape tests | Domain Vitest project | Continue moving only tests with no external boundary dependency |
Use these dispositions consistently:
- **Moved**: the same material claim now has depth-appropriate ownership.
- **Replaced**: stronger executable evidence supersedes the old assertion.
- **Consolidated**: one retained test proves a claim previously duplicated.
- **Intentionally removed**: the claim was not public or independent; the row
must still name the retained evidence or explain why none is warranted.
- **In progress**: classification exists but the replacement audit is not
complete. No final-cutover row may retain this disposition.
The exact old-to-new inventory is mechanically enforced by the project
classification contract and retained in Git rename metadata. Real-system lanes,
coverage, repeated-run stability, and benchmark timing are release evidence;
they are reported separately because they depend on the execution host and
installed external systems.
+106
View File
@@ -0,0 +1,106 @@
# Testing REA
REA's test suite follows one rule: use the lowest behavioral depth that can
prove a claim. Tests are grouped into native Vitest projects so ownership,
allowed dependencies, and runtime cost are visible from their paths.
## Behavioral depths
| Depth | Location | What it proves |
| ------------ | ----------------------- | ----------------------------------------------------------------------------------------------------- |
| Module | `src/**/*.test.ts` | One domain, service, or adapter through its narrow public surface |
| Composition | `tests/composition/**` | Provider-neutral session and registry wiring; filesystem access is limited to fixture materialization |
| Boundary | `tests/boundary/**` | Exactly one production filesystem, process, network, browser, CLI, or provider boundary |
| MCP boundary | `tests/boundary/mcp/**` | MCP transport and tool-contract boundaries with isolated sessions and explicit cleanup |
| Acceptance | `tests/acceptance/**` | A complete compiled CLI or MCP workflow |
| Conformance | `tests/conformance/**` | Shared provider contracts, parameterized by declared capabilities and explicit opt-outs |
| Evaluation | `tests/evaluation/**` | Deterministic evaluator parsing, scoring, and report generation |
Colocated domain tests may import only their owning domain and inward
dependencies. They do not construct sessions or servers and do not start
subprocesses or network listeners. Colocated application tests exercise one
service through explicit ports and recording adapters, never a CLI or MCP
entrypoint. Composition tests may assemble provider-neutral sessions and
registries but do not cross production filesystem, process, socket, or browser boundaries.
Boundary tests cross one production boundary. Only acceptance tests assemble
the complete runtime or invoke the compiled product surface.
`tests/process-global/**` is reserved for cases with a demonstrated dependency
on process-global state. Those tests run without file parallelism. Reusable,
test-scoped fixtures live under `tests/support/**`; immutable source artifacts
remain under `tests/fixtures/**`.
The process-global Vitest configuration contract rejects new direct temporary-root
creation outside the workspace seam and its narrowly documented boundary/package
exceptions.
Real Hopper, Ghidra, browser, package, managed-code, and controlled-replay
claims belong to their explicit `npm run verify:*` lanes. They are not inferred
from mocks or folded into the deterministic local gate. Real model trials are
manual; Vitest covers only deterministic evaluator logic.
## Developer commands
`npm test` runs every deterministic project once. The narrower feedback loops
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 |
| `npm run test:changed` | Changed tests in non-serial projects via Vitest's import graph |
| `npm run test:watch` | Changed domain, service, adapter, composition, boundary, MCP boundary, conformance, and evaluation tests in watch mode |
| `npm run test:watch:all` | Changed tests from every deterministic project in watch mode |
| `npm run check:changed` | Cached static checks followed by changed tests |
| `npm run check:pr` | Static, generated-document, and complete deterministic PR gate |
Changed-test selection is a fast feedback aid, not release evidence. It can
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:
```bash
NODE_COMPILE_CACHE=.cache/node-compile npm test
```
Do not report the warm result as a cold-suite improvement, and do not enable
the cache in coverage or benchmark CI without first showing that its
instrumentation remains equivalent.
## Coverage and timing
CI owns coverage. The aggregate floors are 65% statements, 60% branches, 60%
functions, and 68% lines. `src/domain/**` must reach 80% statements, 75%
branches, 75% functions, and 80% lines. `src/contracts/**` must reach 85%
statements, 80% branches, 80% functions, and 85% lines. Thresholds are
glob-specific rather than per-file and are never updated automatically.
Coverage does not replace named boundary, acceptance, or real-provider scenario
matrices.
CI runs four native Vitest shards without retries. Each shard emits a blob
report; the merge job produces aggregate coverage plus JUnit and JSON timing
reports, uploads them together, and writes the slowest files to the workflow
summary. Static checks, documentation, build, package verification, and
real-system lanes remain separate jobs so one kind of evidence cannot stand in
for another.
The PR acceptance target is a median `npm run check:pr` wall time below three
minutes across three warm-build runs on the benchmark host. Keep Vitest caches
cold unless separately identified. The final cutover also requires three
consecutive deterministic passes without retries, `npm run verify:package`, and
the applicable real-system lanes.
+1 -1
View File
@@ -12,7 +12,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
11,src/domain/errors.ts:ArtifactOperationError,Artifact inventory or extraction fails,"Artifact {operation} failed: {reason} at {logicalPath} (declared_sha256=...)",Exposes paths hashes and internal reason,"Reason-specific plain message with safe recovery step.",implemented,focused assertions passed; full `npm run check` passed
12,src/domain/errors.ts:EvidenceFileError,Evidence file operation fails,"Evidence bundle {operation} failed: {reason}",Internal reason token gives weak recovery,"Reason-specific plain message with safe recovery step.",implemented,focused assertions passed; full `npm run check` passed
13,src/domain/errors.ts:EvidenceIntegrityError,Evidence validation fails,Raw constructor message,May expose validation internals and lacks recovery,"Evidence is invalid or has changed. Recreate or re-import it, then try again.",implemented,focused assertions passed; full `npm run check` passed
14,src/domain/errors.ts:UnknownRegistryError,Evidence-state mutation fails,"Residual unknown registry mutation failed: {reason}",Exposes internal subsystem and state token,"Evidence state changed before the update completed. Refresh the current state and try again.",implemented,focused assertions passed; full `npm run check` passed
14,src/domain/errors.ts:UnknownRegistryError,Evidence-state mutation fails,"Residual unknown registry mutation failed: {reason}",Exposes internal subsystem and state token,"Missing IDs identify the session and retry; concurrent revision conflicts refresh current state.",implemented,focused assertions passed; full `npm run check` passed
15,src/domain/errors.ts:ProcessCaptureError,Process capture fails,Raw constructor message,May expose process and cleanup internals,"Process capture could not complete. Check command access and limits, then try again.",implemented,focused assertions passed; full `npm run check` passed
16,src/domain/errors.ts:AnalysisTimeoutError,Analysis deadline expires,"Analysis operation timed out after {timeoutMs}ms: {operation}",Exposes internal deadline and operation; weak recovery,"Analysis took too long. Try a smaller request, then run `rea doctor` if it continues.",implemented,focused assertions passed; full `npm run check` passed
17,src/domain/errors.ts:HopperTimeoutError,Provider deadline expires,"Hopper did not respond within {timeoutMs}ms",Exposes provider and internal deadline,"Analysis took too long. Try a smaller request, then run `rea doctor` if it continues.",implemented,focused assertions passed; full `npm run check` passed
1 harm_rank location trigger current_copy user_risk proposed_replacement implementation_status verification_result
12 11 src/domain/errors.ts:ArtifactOperationError Artifact inventory or extraction fails Artifact {operation} failed: {reason} at {logicalPath} (declared_sha256=...) Exposes paths hashes and internal reason Reason-specific plain message with safe recovery step. implemented focused assertions passed; full `npm run check` passed
13 12 src/domain/errors.ts:EvidenceFileError Evidence file operation fails Evidence bundle {operation} failed: {reason} Internal reason token gives weak recovery Reason-specific plain message with safe recovery step. implemented focused assertions passed; full `npm run check` passed
14 13 src/domain/errors.ts:EvidenceIntegrityError Evidence validation fails Raw constructor message May expose validation internals and lacks recovery Evidence is invalid or has changed. Recreate or re-import it, then try again. implemented focused assertions passed; full `npm run check` passed
15 14 src/domain/errors.ts:UnknownRegistryError Evidence-state mutation fails Residual unknown registry mutation failed: {reason} Exposes internal subsystem and state token Evidence state changed before the update completed. Refresh the current state and try again. Missing IDs identify the session and retry; concurrent revision conflicts refresh current state. implemented focused assertions passed; full `npm run check` passed
16 15 src/domain/errors.ts:ProcessCaptureError Process capture fails Raw constructor message May expose process and cleanup internals Process capture could not complete. Check command access and limits, then try again. implemented focused assertions passed; full `npm run check` passed
17 16 src/domain/errors.ts:AnalysisTimeoutError Analysis deadline expires Analysis operation timed out after {timeoutMs}ms: {operation} Exposes internal deadline and operation; weak recovery Analysis took too long. Try a smaller request, then run `rea doctor` if it continues. implemented focused assertions passed; full `npm run check` passed
18 17 src/domain/errors.ts:HopperTimeoutError Provider deadline expires Hopper did not respond within {timeoutMs}ms Exposes provider and internal deadline Analysis took too long. Try a smaller request, then run `rea doctor` if it continues. implemented focused assertions passed; full `npm run check` passed
@@ -1,20 +1,20 @@
{
"schema_version": 1,
"manifest_id": "ecm_ee8e5f2f4ea090052051d2994cbff148d9c796e1bc28b2fdc145aa5fa48cd248",
"manifest_id": "ecm_c44391ed8e365e21871664422aa38fa6c12598006cde731d65a9d9e271b0fa5f",
"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": "6a910f067c946d545f5eb711eacbdf804839dbfb6b3b7b6ea91769be37ae47ce"
"sha256": "97a5bc5d29f162fe171ddbb8f4f5ba9fcba34ad336d4cdcae0c564223aa4cfe8"
}
],
"claims": [
+5 -1
View File
@@ -2,9 +2,13 @@
"$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": {
"src/domain/mobileApplicationInvestigation.ts": ["types"],
"src/domain/objcSwiftMetadata.ts": ["types"]
},
"rules": {
"files": "error",
"dependencies": "error",
+15 -15
View File
@@ -1,12 +1,12 @@
{
"name": "rea-agents",
"version": "2.5.0",
"version": "3.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rea-agents",
"version": "2.5.0",
"version": "3.1.0",
"license": "MIT",
"dependencies": {
"@babel/parser": "8.0.4",
@@ -15,8 +15,8 @@
"@electron/asar": "4.2.0",
"@jridgewell/trace-mapping": "0.3.31",
"@lydell/node-pty": "1.1.0",
"@modelcontextprotocol/client": "2.0.0-beta.4",
"@modelcontextprotocol/server": "2.0.0-beta.4",
"@modelcontextprotocol/client": "2.0.0",
"@modelcontextprotocol/server": "2.0.0",
"@xterm/addon-serialize": "0.14.0",
"@xterm/headless": "6.0.0",
"@zip.js/zip.js": "2.8.26",
@@ -369,12 +369,12 @@
]
},
"node_modules/@modelcontextprotocol/client": {
"version": "2.0.0-beta.4",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0-beta.4.tgz",
"integrity": "sha512-VNHA/UXDk7mCpVl+jOg5B4WMRRD2OEl+it360Lhi6HiCbrIB2f6pZ0cqSDKXQVUtZvqnhdQHzZAM9h8EKWhq/A==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0.tgz",
"integrity": "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw==",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/core": "2.0.0-beta.4",
"@modelcontextprotocol/core": "2.0.0",
"cross-spawn": "^7.0.5",
"eventsource": "^3.0.2",
"eventsource-parser": "^3.0.0",
@@ -387,9 +387,9 @@
}
},
"node_modules/@modelcontextprotocol/core": {
"version": "2.0.0-beta.4",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0-beta.4.tgz",
"integrity": "sha512-nsMXd4wQBKzmph6r+WOhum+mXjDYljTAqwY/XUg3hLtvNOQ8+JVqBSJOVCMJvx9lXhTpTOPrGZ3BuNiaNPjSvg==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz",
"integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==",
"license": "MIT",
"dependencies": {
"zod": "^4.2.0"
@@ -399,12 +399,12 @@
}
},
"node_modules/@modelcontextprotocol/server": {
"version": "2.0.0-beta.4",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0-beta.4.tgz",
"integrity": "sha512-pjMZcNEt1dOq0aJCcY3b7w1Ayh+qmQN2xlfTELcGV9yiAjEGIczBPBLWWW0k0zzo5T8kiv15jtKPsWeHGxLvLg==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz",
"integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/core": "2.0.0-beta.4",
"@modelcontextprotocol/core": "2.0.0",
"zod": "^4.2.0"
},
"engines": {
+35 -16
View File
@@ -1,6 +1,7 @@
{
"name": "rea-agents",
"version": "2.5.0",
"mcpName": "io.github.morluto/rea",
"version": "3.1.0",
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
"license": "MIT",
"repository": {
@@ -43,6 +44,8 @@
"bridge/hopper_bridge.py",
"bridge/ghidra/ReaGhidraBridge.java",
"scripts/rea.mjs",
"scripts/electron-active-hook.cjs",
"scripts/electron-active-hook-boundaries.cjs",
"scripts/hopper-demo-x11.py",
"scripts/prepack.mjs",
"scripts/prepare.mjs",
@@ -54,16 +57,21 @@
"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",
"prebuild": "npm run deps:check && npm run metadata: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 && node scripts/clean-build-output.mjs",
"build": "tsc -p tsconfig.build.json",
"precheck": "npm run deps:check",
"check": "turbo run typecheck lint format:check knip metadata:check",
"precheck:fast": "npm run deps:check",
"check:fast": "turbo run typecheck lint",
"precheck:changed": "npm run deps:check",
"check:changed": "turbo run typecheck lint && npm run test:changed",
"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",
@@ -76,32 +84,42 @@
"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 parallel --project subprocess",
"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",
"test:boundary": "vitest run --project boundary --project mcp-boundary --project process-global",
"pretest:mcp": "npm run build:cached",
"test:mcp": "vitest run --project mcp-boundary",
"pretest:acceptance": "npm run build:cached",
"test:acceptance": "vitest run --project acceptance",
"pretest:changed": "npm run build:cached",
"test:changed": "vitest run --project parallel --project subprocess --changed",
"test:changed": "vitest run --project domain --project services --project adapters --project composition --project boundary --project mcp-boundary --project conformance --project evaluation --changed",
"pretest:integration": "npm run build:cached",
"test:integration": "vitest run --project serial-integration",
"test:integration": "vitest run --project boundary --project mcp-boundary --project process-global --project acceptance",
"pretest:coverage": "npm run build:cached",
"test:coverage": "vitest run --coverage.enabled --coverage.provider=v8",
"test:ci": "npm run test:coverage -- --reporter=default --reporter=junit --outputFile.junit=test-results.xml",
"pretest:ci:shard": "npm run build:cached",
"test:ci:shard": "npm run test:ci:shard:run",
"test:ci:shard:run": "vitest run --coverage.enabled --coverage.provider=v8 --coverage.thresholds.lines=0 --coverage.thresholds.functions=0 --coverage.thresholds.branches=0 --coverage.thresholds.statements=0 --reporter=default --reporter=blob",
"test:ci:merge": "vitest --merge-reports --coverage.enabled --coverage.provider=v8 --reporter=default --reporter=junit --outputFile.junit=test-results.xml",
"test:ci:merge": "vitest --merge-reports --coverage.enabled --coverage.provider=v8",
"pretest:verbose": "npm run build:cached",
"test:verbose": "vitest run --reporter=verbose",
"pretest:watch": "npm run build:cached",
"test:watch": "vitest --project parallel --project subprocess --changed",
"test:watch": "vitest --project domain --project services --project adapters --project composition --project boundary --project mcp-boundary --project conformance --project evaluation --changed",
"pretest:watch:all": "npm run build:cached",
"test:watch:all": "vitest --changed",
"typecheck": "tsc --noEmit",
"verify:test-temp-hygiene": "npm run build:cached && node scripts/verify-test-temp-hygiene.mjs",
"verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs",
"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",
@@ -123,9 +141,10 @@
"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-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:check:built": "node scripts/generate-package-metadata.mjs --check && node scripts/generate-skill-metadata.mjs --check && node scripts/generate-product-catalog.mjs --check && node scripts/generate-completion-ledger.mjs --check && node scripts/check-doc-facts.mjs && node scripts/generate-error-schema.mjs --check",
"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",
"prepare": "node scripts/prepare.mjs"
@@ -144,8 +163,8 @@
"@electron/asar": "4.2.0",
"@jridgewell/trace-mapping": "0.3.31",
"@lydell/node-pty": "1.1.0",
"@modelcontextprotocol/client": "2.0.0-beta.4",
"@modelcontextprotocol/server": "2.0.0-beta.4",
"@modelcontextprotocol/client": "2.0.0",
"@modelcontextprotocol/server": "2.0.0",
"@xterm/addon-serialize": "0.14.0",
"@xterm/headless": "6.0.0",
"@zip.js/zip.js": "2.8.26",
+10
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",
+10
View File
@@ -0,0 +1,10 @@
import { rm } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
await Promise.all([
rm(join(root, "dist"), { recursive: true, force: true }),
rm(join(root, ".cache", "tsconfig.build.tsbuildinfo"), { force: true }),
]);
+394
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 };
+501
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,
});
+15 -4
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([
+194
View File
@@ -0,0 +1,194 @@
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { format } from "prettier";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { McpServer } from "@modelcontextprotocol/server";
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managedWorkflowToolContracts.js";
import { ArtifactProvider } from "../dist/artifacts/ArtifactProvider.js";
import { ManagedStaticProvider } from "../dist/dotnet/ManagedStaticProvider.js";
import { NativeMacOSProvider } from "../dist/native/NativeMacOSProvider.js";
import { toolRegistrationOptions } from "../dist/server/toolRegistrationOptions.js";
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
const arguments_ = new Set(process.argv.slice(2));
for (const argument of arguments_)
if (argument !== "--check")
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,
),
...MANAGED_WORKFLOW_TOOL_CONTRACTS.map(({ name }) => name),
]);
const advertisedTools = await sdkToolCatalog();
const catalog = TOOL_CONTRACTS.map((contract) => {
const advertised = advertisedTools.get(contract.name);
if (advertised === undefined)
throw new Error(`MCP SDK omitted registered tool ${contract.name}`);
return {
name: contract.name,
analysisOperation: [
"official-proxy",
"enhanced",
"native-provider",
"artifact-provider",
"managed-provider",
].includes(contract.kind)
? contract.name
: null,
title: advertised.title,
description: advertised.description,
kind: contract.kind,
requiresSession: sessionToolNames.has(contract.name),
inputSchema: advertised.inputSchema,
outputSchema: advertised.outputSchema,
annotations: advertised.annotations,
effects: contract.effects,
};
});
async function sdkToolCatalog() {
const server = new McpServer({ name: "rea-catalog-generator", version: "0" });
for (const contract of TOOL_CONTRACTS)
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async () => ({
content: [{ type: "text", text: "Catalog-only handler" }],
isError: true,
}),
);
const client = new Client({ name: "rea-catalog-generator", version: "0" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const { tools } = await client.listTools();
return new Map(tools.map((tool) => [tool.name, tool]));
} finally {
await Promise.allSettled([client.close(), server.close()]);
}
}
const auxiliaryProviders = [
new ArtifactProvider(false, false),
new NativeMacOSProvider(undefined, CATALOG_PLATFORM),
new ManagedStaticProvider(),
].map((provider) => ({
identity: provider.identity(),
capabilities: provider.capabilities(),
}));
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 {
CapabilityDescriptor,
ProviderIdentity,
} from "./application/AnalysisProvider.js";
import type { ToolKind } from "./contracts/toolContractTypes.js";
import type { ToolEffects } from "./contracts/toolEffects.js";
interface GeneratedTool {
readonly name: string;
readonly analysisOperation: CapabilityDescriptor["operation"] | null;
readonly title: string;
readonly description: string;
readonly kind: ToolKind;
readonly requiresSession: boolean;
readonly inputSchema: Readonly<Record<string, unknown>>;
readonly outputSchema: Readonly<Record<string, unknown>>;
readonly annotations: ToolAnnotations;
readonly effects: ToolEffects;
}
interface GeneratedProvider {
readonly identity: ProviderIdentity;
readonly capabilities: readonly CapabilityDescriptor[];
}
interface GeneratedPayload {
readonly catalog: readonly GeneratedTool[];
readonly auxiliaryProviders: readonly GeneratedProvider[];
}
const GENERATED_PAYLOAD_JSON = ${JSON.stringify(payloadJson)};
const generatedPayload: unknown = JSON.parse(GENERATED_PAYLOAD_JSON);
if (!isGeneratedPayload(generatedPayload))
throw new TypeError("Generated MCP catalog payload is invalid");
/** Generated from TOOL_CONTRACTS; do not edit. */
export const GENERATED_MCP_TOOL_CATALOG = generatedPayload.catalog;
/** Generated lightweight metadata for lazily loaded auxiliary providers. */
export const GENERATED_AUXILIARY_PROVIDERS = generatedPayload.auxiliaryProviders;
function isGeneratedPayload(value: unknown): value is GeneratedPayload {
return (
isRecord(value) &&
Array.isArray(value.catalog) &&
value.catalog.every(isGeneratedTool) &&
Array.isArray(value.auxiliaryProviders) &&
value.auxiliaryProviders.every(isGeneratedProvider)
);
}
function isGeneratedTool(value: unknown): value is GeneratedTool {
return (
isRecord(value) &&
typeof value.name === "string" &&
(typeof value.analysisOperation === "string" ||
value.analysisOperation === null) &&
typeof value.title === "string" &&
typeof value.description === "string" &&
typeof value.kind === "string" &&
typeof value.requiresSession === "boolean" &&
isRecord(value.inputSchema) &&
isRecord(value.outputSchema) &&
isRecord(value.annotations) &&
isRecord(value.effects)
);
}
function isGeneratedProvider(value: unknown): value is GeneratedProvider {
return (
isRecord(value) &&
isRecord(value.identity) &&
Array.isArray(value.capabilities)
);
}
function isRecord(
value: unknown,
): value is Readonly<Record<string, unknown>> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
`,
{ parser: "typescript" },
);
await ensureGeneratedFile({
path: join(root, "src/generatedMcpToolCatalog.ts"),
source,
check: arguments_.has("--check"),
generateCommand: "npm run mcp-catalog:generate",
});
+47 -20
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,
};
};
+13
View File
@@ -1,6 +1,9 @@
import { createHash } from "node:crypto";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import canonicalize from "canonicalize";
const load = (root, path) => import(pathToFileURL(join(root, path)).href);
/** Sort tool/provider names alphabetically. */
@@ -9,6 +12,14 @@ export const names = (contracts) =>
.map(({ name }) => name)
.sort((left, right) => left.localeCompare(right));
/** Return the stable digest for a generated catalog projection. */
export const digest = (value) => {
const encoded = canonicalize(value);
if (encoded === undefined)
throw new TypeError("Catalog projection is not canonical JSON");
return createHash("sha256").update(encoded).digest("hex");
};
/** Throw when two name lists diverge. */
export const assertSameNames = (label, actual, expected) => {
const actualSet = new Set(actual);
@@ -44,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",
@@ -59,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",
+62
View File
@@ -0,0 +1,62 @@
interface McpStartupPolicy {
readonly initializeBudgetMs: number;
readonly toolsListBudgetMs: number;
readonly firstCatalogBudgetMs: number;
readonly doctorDeadlineMs: number;
}
interface McpProcessOptions {
readonly command: string;
readonly args: readonly string[];
readonly environment: Readonly<Record<string, string | undefined>>;
readonly policy: McpStartupPolicy;
}
interface McpModuleProfileOptions extends McpProcessOptions {
readonly packageName: string;
}
interface RssMeasurement {
readonly supported: boolean;
readonly method: string | null;
readonly peak_bytes: number | null;
}
/** Measure the first usable MCP catalog through a child-process boundary. */
export function measureMcpStartup(options: McpProcessOptions): Promise<{
readonly process_started_to_initialized_ms: number;
readonly initialized_to_tools_list_ms: number;
readonly process_started_to_tools_list_ms: number;
readonly listed_tool_count: number;
readonly rss: RssMeasurement;
}>;
/** Profile ESM file loading in a separate diagnostic MCP child. */
export function profileMcpModuleLoading(
options: McpModuleProfileOptions,
): Promise<{
readonly supported: true;
readonly method: "node-debug-esm";
readonly unique_file_modules: number;
readonly unique_package_modules: number;
readonly unique_dependency_modules: number;
readonly debug_lines: number;
readonly truncated: boolean;
}>;
/** Parse Linux procfs VmRSS text into bytes. */
export function parseLinuxVmRssBytes(status: string): number | null;
/** Collect unique file-backed ESM module URLs from Node diagnostics. */
export function createEsmModuleProfileCollector(packageName?: string): {
append(chunk: string): void;
result(): {
readonly supported: true;
readonly method: "node-debug-esm";
readonly unique_file_modules: number;
readonly unique_package_modules: number;
readonly unique_dependency_modules: number;
readonly debug_lines: number;
readonly truncated: boolean;
};
};
+217
View File
@@ -0,0 +1,217 @@
import { readFileSync } from "node:fs";
import { performance } from "node:perf_hooks";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
const RSS_SAMPLE_INTERVAL_MS = 10;
const MAXIMUM_PROFILE_BUFFER_BYTES = 2 * 1_024 * 1_024;
const MAXIMUM_PROFILE_MODULES = 10_000;
/**
* Measure the first usable MCP catalog through the installed process boundary.
*/
export async function measureMcpStartup({
command,
args,
environment,
policy,
}) {
const transport = new StdioClientTransport({
command,
args,
env: environment,
stderr: "pipe",
});
let stderr = "";
transport.stderr?.on("data", (chunk) => {
stderr = boundedAppend(stderr, chunk.toString());
});
const rss = createLinuxRssSampler(() => transport.pid);
const client = new Client({
name: "package-startup-probe",
version: "1.0.0",
});
const startedAt = performance.now();
rss.start();
try {
await client.connect(transport, { timeout: policy.initializeBudgetMs });
const initializedAt = performance.now();
const tools = await client.listTools(undefined, {
timeout: policy.toolsListBudgetMs,
});
const listedAt = performance.now();
rss.stop();
const measurement = {
process_started_to_initialized_ms: elapsed(startedAt, initializedAt),
initialized_to_tools_list_ms: elapsed(initializedAt, listedAt),
process_started_to_tools_list_ms: elapsed(startedAt, listedAt),
listed_tool_count: tools.tools.length,
rss: rss.result(),
};
assertStartupBudgets(measurement, policy);
return measurement;
} catch (cause) {
throw new Error(`packaged MCP startup probe failed: ${stderr}`, { cause });
} finally {
rss.stop();
await Promise.allSettled([client.close(), transport.close()]);
}
}
/**
* Profile ESM file loading in a separate diagnostic child.
*/
export async function profileMcpModuleLoading({
command,
args,
environment,
policy,
packageName,
}) {
const profile = createEsmModuleProfileCollector(packageName);
const transport = new StdioClientTransport({
command,
args,
env: { ...environment, NODE_DEBUG: "esm" },
stderr: "pipe",
});
transport.stderr?.on("data", (chunk) => profile.append(chunk.toString()));
const client = new Client({
name: "package-module-profile",
version: "1.0.0",
});
try {
await client.connect(transport, {
timeout: policy.doctorDeadlineMs,
});
await client.listTools(undefined, {
timeout: policy.toolsListBudgetMs,
});
return profile.result();
} finally {
await Promise.allSettled([client.close(), transport.close()]);
}
}
/**
* Parse Linux procfs VmRSS text into bytes.
*/
export const parseLinuxVmRssBytes = (status) => {
const match = /^VmRSS:\s+(\d+)\s+kB$/mu.exec(status);
return match === null ? null : Number(match[1]) * 1_024;
};
/**
* Collect unique file-backed ESM module URLs from Node's diagnostic stream.
*/
export const createEsmModuleProfileCollector = (packageName = "rea-agents") => {
const modules = new Set();
let buffer = "";
let debugLines = 0;
let truncated = false;
const consume = (line) => {
if (line.startsWith("ESM ")) debugLines += 1;
for (const match of line.matchAll(/file:\/\/\/[^\s'",}\])]+/gu)) {
if (modules.size >= MAXIMUM_PROFILE_MODULES) {
truncated = true;
break;
}
modules.add(match[0]);
}
};
return {
append(chunk) {
buffer += chunk;
if (Buffer.byteLength(buffer) > MAXIMUM_PROFILE_BUFFER_BYTES) {
buffer = buffer.slice(-MAXIMUM_PROFILE_BUFFER_BYTES);
truncated = true;
}
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) consume(line);
},
result() {
if (buffer.length > 0) consume(buffer);
const values = [...modules];
const packageSegment = `/node_modules/${packageName}/`;
return {
supported: true,
method: "node-debug-esm",
unique_file_modules: values.length,
unique_package_modules: values.filter((value) =>
value.includes(packageSegment),
).length,
unique_dependency_modules: values.filter(
(value) =>
value.includes("/node_modules/") && !value.includes(packageSegment),
).length,
debug_lines: debugLines,
truncated,
};
},
};
};
const createLinuxRssSampler = (pid) => {
if (process.platform !== "linux")
return {
start() {},
stop() {},
result: () => ({
supported: false,
method: null,
peak_bytes: null,
}),
};
let peakBytes = null;
let timer;
const sample = () => {
const currentPid = pid();
if (currentPid === null) return;
try {
const current = parseLinuxVmRssBytes(
readFileSync(`/proc/${String(currentPid)}/status`, "utf8"),
);
if (current !== null && (peakBytes === null || current > peakBytes))
peakBytes = current;
} catch {
// The short-lived child may exit between resolving its PID and reading.
}
};
return {
start() {
sample();
timer = setInterval(sample, RSS_SAMPLE_INTERVAL_MS);
timer.unref();
},
stop() {
if (timer !== undefined) clearInterval(timer);
sample();
},
result: () => ({
supported: true,
method: "linux-proc-status-vmrss-sampled",
peak_bytes: peakBytes,
}),
};
};
const assertStartupBudgets = (measurement, policy) => {
if (measurement.process_started_to_initialized_ms > policy.initializeBudgetMs)
throw new Error("packaged MCP exceeded the initialize regression budget");
if (measurement.initialized_to_tools_list_ms > policy.toolsListBudgetMs)
throw new Error("packaged MCP exceeded the tools/list regression budget");
if (
measurement.process_started_to_tools_list_ms > policy.firstCatalogBudgetMs
)
throw new Error(
"packaged MCP exceeded the first-catalog regression budget",
);
};
const elapsed = (startedAt, completedAt) =>
Math.round((completedAt - startedAt) * 100) / 100;
const boundedAppend = (current, chunk) =>
`${current}${chunk}`.slice(-64 * 1_024);
+3
View File
@@ -46,6 +46,9 @@ export interface ProductCatalog {
}
export function createProductCatalog(root: string): Promise<ProductCatalog>;
export function providerCatalogDigest(
providers: ProductCatalog["providers"],
): string;
export function createCliInventory(cli: unknown): {
readonly primary: readonly string[];
readonly aliases: readonly {
+10 -3
View File
@@ -1,6 +1,6 @@
import { format } from "prettier";
import { assertSameNames, loadSources } from "./catalog-core.mjs";
import { assertSameNames, digest, loadSources } from "./catalog-core.mjs";
import {
createCliInventory,
cliCommandDescriptionIssues,
@@ -15,6 +15,9 @@ export {
cliCommandOptionNames,
};
/** Return the stable digest for the source-derived provider projection. */
export const providerCatalogDigest = (providers) => digest(providers);
/** Project current runtime contracts into deterministic, machine-readable facts. */
export const createProductCatalog = async (root) => {
const sources = await loadSources(root);
@@ -25,6 +28,7 @@ export const createProductCatalog = async (root) => {
sources.catalogIdentity.CLI_COMMAND_NAMES,
);
const tools = toolFamilyCatalog(sources);
const providers = providerCatalog(sources);
const metadata = sources.packageMetadata.PACKAGE_METADATA;
return {
catalog_schema_version: 1,
@@ -39,7 +43,7 @@ export const createProductCatalog = async (root) => {
skill_version: metadata.skillVersion,
},
tools,
providers: providerCatalog(sources),
providers,
setup_clients: sources.supportedClients.SUPPORTED_CLIENT_DEFINITIONS.map(
({ name, displayName, format }) => ({
id: name,
@@ -56,7 +60,10 @@ export const createProductCatalog = async (root) => {
},
runtime_catalog: {
counts: sources.catalogIdentity.CATALOG_IDENTITY.counts,
digests: sources.catalogIdentity.CATALOG_IDENTITY.digests,
digests: {
...sources.catalogIdentity.CATALOG_IDENTITY.digests,
providers_sha256: providerCatalogDigest(providers),
},
},
};
};
+63
View File
@@ -1,5 +1,68 @@
import { requireFunctionDossierOracle } from "../../dist/application/RealHopperAssertions.js";
/** Require truthful tri-state permissions from real Hopper memory regions. */
export const requireTruthfulMemoryRegions = (segments) => {
if (!Array.isArray(segments) || segments.length === 0) {
throw new Error("list_segments returned no real memory regions");
}
const regions = segments.flatMap((segment) => [
segment,
...(Array.isArray(segment.sections) ? segment.sections : []),
]);
for (const region of regions) {
for (const permission of ["readable", "writable", "executable"]) {
if (![true, false, null].includes(region?.[permission])) {
throw new Error(`list_segments omitted tri-state ${permission}`);
}
}
if (
region.provenance !== "hopper-public-python-api" ||
region.permissions?.available !== false ||
typeof region.permissions.reason !== "string"
) {
throw new Error("list_segments omitted permission provenance");
}
}
};
/** Require correlated start and terminal progress from the real bridge. */
export function requireBridgeProgress(updates) {
const messages = updates.map(({ message }) => message);
if (
!messages.some(
(message) =>
message === "Hopper bridge started request" ||
message.includes("started on Hopper's serial bridge"),
) ||
!messages.some(
(message) =>
message === "Hopper bridge completed request" ||
message.includes("Hopper bridge completed request"),
)
)
throw new Error(
`The MCP client did not observe correlated Hopper bridge progress: ${JSON.stringify(updates)}`,
);
}
/** Require one sanitized invalid-request diagnostic from the real bridge. */
export function requireBridgeDiagnostic(chunks) {
const diagnostics = chunks
.join("")
.split("\n")
.filter((line) => line.trim().length > 0)
.map((line) => JSON.parse(line));
if (
!diagnostics.some(
(diagnostic) =>
diagnostic.type === "bridge-diagnostic" &&
diagnostic.category === "invalid_request" &&
diagnostic.message === "Invalid Hopper bridge request",
)
)
throw new Error("The MCP runtime omitted the correlated Hopper diagnostic");
}
/** Verify named positive semantics from the source-owned C fixture. */
export async function verifyRealHopperFixture({
client,
+4 -7
View File
@@ -28,8 +28,8 @@ export const runWithStatus = async (command, args, env) => {
/** Parse JSON from a command output string. */
export const json = (text) => JSON.parse(text);
/** Verify that the advertised MCP catalog matches the session's available tools. */
export const verifyAvailableToolCatalog = async (client, options) => {
/** Verify that the complete advertised MCP catalog matches session metadata. */
export const verifyCompleteToolCatalog = async (client, options) => {
const listed = await client.listTools(undefined, options);
const status = await client.callTool(
{ name: "binary_session", arguments: { detail: "full" } },
@@ -38,14 +38,11 @@ export const verifyAvailableToolCatalog = async (client, options) => {
const availability = status.structuredContent?.result?.tool_availability;
if (!Array.isArray(availability))
throw new Error("packaged MCP omitted tool availability");
const expected = availability
.filter(({ available }) => available === true)
.map(({ name }) => name)
.sort();
const expected = availability.map(({ name }) => name).sort();
const observed = listed.tools.map(({ name }) => name).sort();
if (JSON.stringify(observed) !== JSON.stringify(expected))
throw new Error(
"packaged MCP tool inventory diverged from session availability",
"packaged MCP tool inventory diverged from session metadata",
);
return observed;
};
+147
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;
}
+57 -35
View File
@@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url";
import { createPackage } from "@electron/asar";
import { evaluateCodexEvents } from "../dist/evaluation/CodexAgentEval.js";
import { MCP_STARTUP_POLICY } from "../dist/mcpStartupPolicy.js";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
@@ -33,7 +34,16 @@ try {
skillDestination,
{ recursive: true },
);
const targets = await createTargets(fixtureRoot);
const requestedScenarioIds = new Set(
(process.env.REA_AGENT_EVAL_SCENARIOS ?? "")
.split(",")
.map((value) => value.trim())
.filter((value) => value.length > 0),
);
const targets = await createTargets(
fixtureRoot,
requestedScenarioIds.size === 0 || requestedScenarioIds.has("managed"),
);
const allScenarios = [
{
id: "native",
@@ -93,13 +103,23 @@ try {
prompt:
"A user-owned page is already open at http://127.0.0.1:3000 through the approved local debugging endpoint http://127.0.0.1:9222. Inspect what is available without navigating or executing page code, and report any authority or availability limits.",
},
{
id: "navigation-context",
expectedFirstTool: "get_navigation_context",
requiresEvidence: false,
requiredAnswerTermGroups: [["unavailable", "could not", "session"]],
prompt:
"Report the selected document, current address, and containing procedure from the current native-analysis session in one coherent request. If no native session is available, report that limitation plainly.",
},
{
id: "address-context",
expectedFirstTool: "inspect_address_context",
requiresEvidence: false,
requiredAnswerTermGroups: [["unavailable", "could not", "session"]],
prompt:
"Inspect explicit address 0x401000 in the current native-analysis session. Report its analyzed name, containing procedure, regular comment, inline comment, and matching bookmarks. If no native session is available, report that limitation plainly.",
},
];
const requestedScenarioIds = new Set(
(process.env.REA_AGENT_EVAL_SCENARIOS ?? "")
.split(",")
.map((value) => value.trim())
.filter((value) => value.length > 0),
);
const scenarios =
requestedScenarioIds.size === 0
? allScenarios
@@ -233,7 +253,7 @@ try {
await rm(evaluationRoot, { recursive: true, force: true });
}
async function createTargets(root) {
async function createTargets(root, includeManaged) {
const javascript = join(root, "desktop-app");
await mkdir(join(javascript, "renderer"), { recursive: true });
await Promise.all([
@@ -274,33 +294,35 @@ async function createTargets(root) {
),
]);
const managedProject = join(root, "managed-project");
const managedOutput = join(root, "managed-output");
await mkdir(managedProject, { recursive: true });
await Promise.all([
writeFile(
join(managedProject, "AgentEval.csproj"),
'<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net8.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup></Project>\n',
),
writeFile(
join(managedProject, "Program.cs"),
'namespace AgentEval; public sealed record Profile(string Id); public static class Program { public static void Main() => Console.WriteLine(new Profile("fixture")); }\n',
),
]);
await runProcess(
"dotnet",
[
"build",
join(managedProject, "AgentEval.csproj"),
"--configuration",
"Release",
"--output",
managedOutput,
"--nologo",
],
root,
120_000,
);
if (includeManaged) {
const managedProject = join(root, "managed-project");
await mkdir(managedProject, { recursive: true });
await Promise.all([
writeFile(
join(managedProject, "AgentEval.csproj"),
'<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net8.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup></Project>\n',
),
writeFile(
join(managedProject, "Program.cs"),
'namespace AgentEval; public sealed record Profile(string Id); public static class Program { public static void Main() => Console.WriteLine(new Profile("fixture")); }\n',
),
]);
await runProcess(
"dotnet",
[
"build",
join(managedProject, "AgentEval.csproj"),
"--configuration",
"Release",
"--output",
managedOutput,
"--nologo",
],
root,
120_000,
);
}
return {
native: "/bin/true",
javascript: javascriptAsar,
@@ -346,7 +368,7 @@ async function runCodex(prompt, investigationRoot) {
"-c",
`mcp_servers.rea.args=${JSON.stringify([join(repositoryRoot, "scripts/rea.mjs"), "mcp"])}`,
"-c",
"mcp_servers.rea.startup_timeout_sec=30",
`mcp_servers.rea.startup_timeout_sec=${String(MCP_STARTUP_POLICY.codexStartupTimeoutSeconds)}`,
"-c",
mcpEnvironmentOverride,
...(optionalModel === undefined ? [] : ["--model", optionalModel]),
+26 -10
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}`,
+3 -3
View File
@@ -344,11 +344,11 @@ try {
assert.equal(reconstructionImport.value.confidence, "inferred");
const runtimePlan = await planManagedRuntimeCorrelationEvidence(
{
policy: {
enabled: true,
policy: () => ({
status: "enabled",
roots: [workspace],
executablePath: runtimeExecutable,
},
}),
authority: runtimeAuthority.value,
},
{
+51
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`,
);
+66 -2
View File
@@ -1,4 +1,5 @@
import { mkdir, writeFile } from "node:fs/promises";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { isDeepStrictEqual } from "node:util";
import { join } from "node:path";
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
@@ -21,12 +22,30 @@ export async function verifyPackageArtifactAndElectron({
environment,
),
);
const repeatedArtifactInventory = json(
await run(
cli,
["inventory-artifact", artifactArchive, "--limit", "500", "--json"],
environment,
),
);
if (
artifactInventory.operation !== "inventory_artifact" ||
artifactInventory.provider?.id !== "rea-artifact-graph" ||
artifactInventory.normalized_result?.manifest?.root_format !== "zip"
artifactInventory.normalized_result?.manifest?.root_format !== "zip" ||
!sameArtifactIdentity(
artifactInventory.normalized_result,
repeatedArtifactInventory.normalized_result,
)
)
throw new Error("packaged artifact inventory CLI failed");
await verifyPackagedArtifactExtraction({
artifactArchive,
artifactInventory,
cli,
environment,
workspace,
});
const applicationRoot = join(workspace, "electron-app");
await mkdir(applicationRoot);
await writeFile(
@@ -73,6 +92,51 @@ export async function verifyPackageArtifactAndElectron({
return { artifactArchive };
}
const sameArtifactIdentity = (left, right) =>
isDeepStrictEqual(left?.manifest, right?.manifest) &&
isDeepStrictEqual(left?.nodes?.items, right?.nodes?.items) &&
isDeepStrictEqual(left?.occurrences?.items, right?.occurrences?.items) &&
isDeepStrictEqual(left?.edges?.items, right?.edges?.items);
const verifyPackagedArtifactExtraction = async ({
artifactArchive,
artifactInventory,
cli,
environment,
workspace,
}) => {
const occurrence =
artifactInventory.normalized_result?.occurrences?.items?.find(
({ logical_path: path }) => path === "app/main.js",
);
if (occurrence?.occurrence_id === undefined)
throw new Error("packaged artifact inventory omitted the selected member");
const outputRoot = join(workspace, "artifact-extraction");
const extraction = json(
await run(
cli,
[
"extract-artifact",
artifactArchive,
outputRoot,
"--occurrence-ids",
occurrence.occurrence_id,
"--json",
],
environment,
),
);
if (
extraction.operation !== "extract_artifact" ||
extraction.provider?.id !== "rea-artifact-graph" ||
extraction.normalized_result?.containment_verified !== true ||
extraction.normalized_result?.artifacts?.items?.[0]?.relative_path !==
"app/main.js" ||
(await readFile(join(outputRoot, "app/main.js"), "utf8")) !== "main();"
)
throw new Error("packaged artifact extraction CLI failed");
};
const assertRoutedApplicationAnalysis = (analysis) => {
if (
analysis.operation !== "analyze_javascript_application" ||
+23 -14
View File
@@ -5,16 +5,14 @@ import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { MCP_STARTUP_POLICY } from "../dist/mcpStartupPolicy.js";
import * as prompts from "./verify-package-prompts.mjs";
import {
json,
verifyAvailableToolCatalog,
} from "./lib/verify-package-core.mjs";
import { json, verifyCompleteToolCatalog } from "./lib/verify-package-core.mjs";
const execute = promisify(execFile);
const verifyMcpToolsAndPrompts = async (client, mcpOptions) => {
await verifyAvailableToolCatalog(client, mcpOptions);
await verifyCompleteToolCatalog(client, mcpOptions);
await prompts.verifyPromptCatalog(client, mcpOptions, prompts.names);
await prompts.verifyPromptCompletion(client, mcpOptions, false);
};
@@ -42,11 +40,21 @@ const verifyMcpReplay = async (client, mcpOptions, investigationReplay) => {
};
const verifyMcpTargetFree = async (client, mcpOptions) => {
const names = (await client.listTools(undefined, mcpOptions)).tools.map(
({ name }) => name,
const status = await client.callTool(
{ name: "binary_session", arguments: { detail: "full" } },
mcpOptions,
);
if (names.includes("current_document"))
throw new Error("packaged target-free MCP advertised a target-bound tool");
const currentDocument =
status.structuredContent?.result?.tool_availability?.find(
({ name }) => name === "current_document",
);
if (
currentDocument?.available !== false ||
currentDocument.reason !== "target_required"
)
throw new Error(
"packaged target-free MCP omitted target-bound availability metadata",
);
};
const verifyMcpUnknownProvider = async (client, mcpOptions) => {
@@ -95,11 +103,12 @@ const verifyMcpOpenAndBind = async (client, mcpOptions) => {
};
const verifyMcpLinuxToolAvailability = async (client, mcpOptions) => {
const names = (await client.listTools(undefined, mcpOptions)).tools.map(
({ name }) => name,
const current = await client.callTool(
{ name: "current_document", arguments: {} },
mcpOptions,
);
if (names.includes("current_document"))
throw new Error("packaged Linux MCP advertised unavailable Hopper tools");
if (current.isError !== true)
throw new Error("packaged Linux MCP executed an unavailable Hopper tool");
};
const verifyMcpNonLinuxCurrentDocument = async (
@@ -180,7 +189,7 @@ export async function verifyPackageMcp({
(
await execute(cli, ["mcp", "doctor", "--json"], {
env: environment,
timeout: 30_000,
timeout: MCP_STARTUP_POLICY.doctorDeadlineMs,
})
).stdout,
);
+2
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"),
+27 -1
View File
@@ -4,6 +4,7 @@ import { join } from "node:path";
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
import { PRODUCT_IDENTITY } from "../dist/identity.js";
import { MCP_STARTUP_POLICY } from "../dist/mcpStartupPolicy.js";
import * as prompts from "./verify-package-prompts.mjs";
import { verifyPackagedInvestigation } from "./verify-package-investigation.mjs";
import { verifyPackageArtifactAndElectron } from "./verify-package-artifact.mjs";
@@ -19,6 +20,10 @@ import { verifyPackageSetup } from "./verify-package-setup.mjs";
import { verifyManaged } from "./verify-package-managed.mjs";
import { verifyUnknownProvider } from "./verify-package-unknown-provider.mjs";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
import {
measureMcpStartup,
profileMcpModuleLoading,
} from "./lib/mcp-startup-probe.mjs";
const root = process.cwd();
const verifierRun = createVerifierRun();
@@ -26,6 +31,8 @@ const workspace = await mkdtemp(join(tmpdir(), "rea-package-"));
const evidenceRoot = join(workspace, "evidence");
const referenceRoot = join(workspace, "reference-source");
let tarball;
let mcpStartup;
let mcpModuleLoading;
try {
({ tarball } = await verifyPackagePack({ root, workspace }));
@@ -42,6 +49,25 @@ try {
workspace,
environment: environmentData.environment,
});
mcpStartup = await measureMcpStartup({
command: cli,
args: ["mcp"],
environment: {
...environmentData.environment,
REA_INVESTIGATION_INPUT_ROOTS_JSON: JSON.stringify([]),
},
policy: MCP_STARTUP_POLICY,
});
mcpModuleLoading = await profileMcpModuleLoading({
command: cli,
args: ["mcp"],
environment: {
...environmentData.environment,
REA_INVESTIGATION_INPUT_ROOTS_JSON: JSON.stringify([]),
},
policy: MCP_STARTUP_POLICY,
packageName: PRODUCT_IDENTITY.packageName,
});
const { supportedSetupHost } = await verifyPackageDiscovery({
cli,
environment: environmentData.environment,
@@ -102,7 +128,7 @@ try {
investigationReplay,
});
process.stdout.write(
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), cli: true, analysisCli: true, artifactCli: true, managedCli: true, managedReconstructionCli: true, managedNativeVerificationCli: true, managedRuntimePlanCli: true, managedApplicationGraphCli: true, evidenceCli: true, incurMcpCommand: PRODUCT_IDENTITY.mcpCommand, lifecycleScriptsRequired: false, doctor: "platform-appropriate", setup: supportedSetupHost ? "planned-then-idempotent" : "unsupported-host-rejected", setupPlanReadOnly: supportedSetupHost, existingHopperPreserved: supportedSetupHost, clients: supportedSetupHost ? 3 : 0, backupReadback: supportedSetupHost, failureRecovery: supportedSetupHost, configSymlinkLifecycle: supportedSetupHost, skill: supportedSetupHost, skillReferences: supportedSetupHost, mcpTools: TOOL_CONTRACTS.length, mcpPrompts: prompts.names.length, promptCompletion: true, promptCompletionLifecycle: true, evidenceMcp: true, targetFree: true, targetLifecycle: true, boundedRegexBridge: true })}\n`,
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), cli: true, analysisCli: true, artifactCli: true, managedCli: true, managedReconstructionCli: true, managedNativeVerificationCli: true, managedRuntimePlanCli: true, managedApplicationGraphCli: true, evidenceCli: true, incurMcpCommand: PRODUCT_IDENTITY.mcpCommand, lifecycleScriptsRequired: false, doctor: "platform-appropriate", setup: supportedSetupHost ? "planned-then-idempotent" : "unsupported-host-rejected", setupPlanReadOnly: supportedSetupHost, existingHopperPreserved: supportedSetupHost, clients: supportedSetupHost ? 3 : 0, backupReadback: supportedSetupHost, failureRecovery: supportedSetupHost, configSymlinkLifecycle: supportedSetupHost, skill: supportedSetupHost, skillReferences: supportedSetupHost, mcpTools: TOOL_CONTRACTS.length, mcpPrompts: prompts.names.length, promptCompletion: true, promptCompletionLifecycle: true, evidenceMcp: true, targetFree: true, targetLifecycle: true, boundedRegexBridge: true, mcpStartup, mcpModuleLoading })}\n`,
);
} finally {
if (tarball) await rm(join(root, tarball), { force: true });
+104 -4
View File
@@ -1,13 +1,18 @@
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { mkdtemp, rm } from "node:fs/promises";
import { execFile } from "node:child_process";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { delimiter, join } from "node:path";
import { promisify } from "node:util";
import { lt, rcompare } from "semver";
import { CATALOG_IDENTITY } from "../dist/catalogIdentity.js";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
const verifierRun = createVerifierRun();
const execFileAsync = promisify(execFile);
const version = process.argv[2];
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(version ?? ""))
throw new Error("Usage: node scripts/verify-published-package.mjs <version>");
@@ -39,6 +44,7 @@ transport.stderr?.on("data", (chunk) => {
});
let publishedToolCount = 0;
let upgrade;
try {
await client.connect(transport);
@@ -65,8 +71,11 @@ try {
});
if (status.isError === true)
throw new Error("Published target-free MCP session check failed");
upgrade = await verifyPublishedUpgrade({ version, canaryRoot });
} catch (cause) {
throw new Error(`Published MCP canary failed: ${stderr}`, { cause });
throw new Error(`Published package verification failed: ${stderr}`, {
cause,
});
} finally {
try {
await client.close();
@@ -77,5 +86,96 @@ try {
}
process.stdout.write(
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), package: "rea-agents", version, mcpTools: publishedToolCount, canonicalMcpTools: CATALOG_IDENTITY.counts.mcp_tools })}\n`,
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), package: "rea-agents", version, mcpTools: publishedToolCount, canonicalMcpTools: CATALOG_IDENTITY.counts.mcp_tools, upgrade })}\n`,
);
async function verifyPublishedUpgrade({ version: targetVersion, canaryRoot }) {
const previousVersion = await findPreviousPublishedVersion(targetVersion);
const prefix = join(canaryRoot, "upgrade-prefix");
const home = join(canaryRoot, "upgrade-home");
const environment = {
...process.env,
HOME: home,
npm_config_cache: join(canaryRoot, "upgrade-cache"),
npm_config_prefix: prefix,
PATH: `${join(prefix, "bin")}${delimiter}${process.env.PATH ?? ""}`,
};
await execFileAsync(
"npm",
[
"install",
"--global",
"--ignore-scripts",
"--prefix",
prefix,
`rea-agents@${previousVersion}`,
],
{ cwd: canaryRoot, env: environment, maxBuffer: 16 * 1024 * 1024 },
);
const { stdout } = await execFileAsync(
"npx",
["rea-agents", "upgrade", "--json"],
{ cwd: canaryRoot, env: environment, maxBuffer: 16 * 1024 * 1024 },
);
const result = JSON.parse(stdout);
if (
result.status !== "upgraded" ||
result.previousVersion !== previousVersion ||
result.latestVersion !== targetVersion
)
throw new Error(
`npx rea-agents upgrade did not upgrade ${previousVersion} to ${targetVersion}: ${stdout}`,
);
const npmRoot = (
await execFileAsync("npm", ["root", "--global"], {
cwd: canaryRoot,
env: environment,
})
).stdout.trim();
const installed = JSON.parse(
await readFile(join(npmRoot, "rea-agents", "package.json"), "utf8"),
);
if (installed.version !== targetVersion)
throw new Error(
`npx rea-agents upgrade installed ${installed.version}, expected ${targetVersion}`,
);
const help = (
await execFileAsync(join(prefix, "bin", "rea-agents"), ["--help"], {
cwd: canaryRoot,
env: environment,
})
).stdout;
if (!help.includes("setup"))
throw new Error("the upgraded rea-agents executable did not start");
return {
status: result.status,
previousVersion,
latestVersion: result.latestVersion,
installedVersion: installed.version,
};
}
async function findPreviousPublishedVersion(targetVersion) {
const { stdout } = await execFileAsync(
"npm",
["view", "rea-agents", "versions", "--json"],
{ maxBuffer: 16 * 1024 * 1024 },
);
const versions = JSON.parse(stdout);
const previous = versions
.filter(
(candidate) =>
typeof candidate === "string" && lt(candidate, targetVersion),
)
.sort(rcompare)[0];
if (previous === undefined)
throw new Error(
`No published rea-agents version precedes ${targetVersion}`,
);
return previous;
}
+1
View File
@@ -114,6 +114,7 @@ try {
observation_ms: 1_000,
include_accessibility_text: true,
include_script_sources: true,
source_capture_approved: true,
include_console_text: true,
console_text_approved: true,
include_json_body_shapes: true,
+113
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`);
+87 -2
View File
@@ -55,9 +55,11 @@ export function assertDossier(
expectedString = null,
requireAssembly = false,
requireMultiBlock = false,
requireJumpTable = false,
},
) {
assertDossierProcedure(dossier, address);
assertNativeApiBoundary(dossier.native_api, requireJumpTable);
assertDossierReferences(dossier);
assertDossierCallees(dossier, expectedCallees);
assertDossierStrings(dossier, expectedString);
@@ -65,6 +67,78 @@ export function assertDossier(
assertDossierMultiBlock(dossier, requireMultiBlock);
}
export function assertDenseSwitchDossier(dossier, address) {
assertDossier(dossier, {
address,
requireAssembly: true,
requireMultiBlock: true,
});
const truncated = dossier.native_api.jump_tables.find(
({ mappings_truncated: mappingsTruncated }) => mappingsTruncated,
);
if (
truncated === undefined ||
truncated.mappings.length !== 32 ||
truncated.mappings.some(
({ target_address: target, evidence }) =>
!/^0x[0-9a-f]+$/u.test(target) ||
!evidence.some(({ kind }) => kind === "jump-table"),
)
)
throw new Error(
`Ghidra dense jump-table truncation drifted: ${JSON.stringify(dossier.native_api.jump_tables)}`,
);
}
function assertNativeApiBoundary(observed, requireJumpTable) {
if (
observed?.available !== true ||
observed.provenance !== "ghidra-high-function" ||
observed.parameters_truncated !== false ||
observed.jump_tables_truncated !== false ||
!["low", "medium", "high"].includes(observed.return_type?.confidence) ||
!observed.return_type?.evidence?.some(
({ source }) => source === "ghidra-high-function",
) ||
observed.pseudocode?.classification !== "decompiler-generated-non-source" ||
observed.pseudocode.compilable !== false ||
!observed.decompiler_artifacts?.includes("pointer-arithmetic")
)
throw new Error(
`Ghidra native API boundary drifted: ${JSON.stringify(observed)}`,
);
if (!requireJumpTable) return;
if (
observed.jump_tables.length === 0 ||
!observed.jump_tables.some(
({
data_sources: sources,
data_sources_truncated: sourcesTruncated,
mappings,
mappings_truncated: mappingsTruncated,
}) =>
!sourcesTruncated &&
!mappingsTruncated &&
sources.length > 0 &&
sources.every(
({ address, evidence }) =>
/^0x[0-9a-f]+$/u.test(address) &&
evidence.some(({ kind }) => kind === "jump-table"),
) &&
mappings.length > 0 &&
mappings.every(
({ target_address: target, data_addresses: addresses, evidence }) =>
/^0x[0-9a-f]+$/u.test(target) &&
addresses.every((address) => /^0x[0-9a-f]+$/u.test(address)) &&
evidence.some(({ kind }) => kind === "jump-table"),
),
)
)
throw new Error(
`Ghidra jump-table evidence drifted: ${JSON.stringify(observed.jump_tables)}`,
);
}
function assertDossierProcedure(dossier, address) {
if (
dossier.procedure.address !== address ||
@@ -144,6 +218,14 @@ export function dossierSummary(dossier) {
outgoing_references: dossier.outgoing_references.returned,
referenced_strings: dossier.referenced_strings.returned,
basic_blocks: dossier.basic_blocks.returned,
native_parameters:
dossier.native_api.available === true
? dossier.native_api.parameters.length
: null,
jump_tables:
dossier.native_api.available === true
? dossier.native_api.jump_tables.length
: null,
};
}
@@ -189,12 +271,15 @@ export async function assertCleanup(coordinates) {
}
export function assertDebugFixture(observed) {
const expectedDocument = `target-${observed.target.sha256.slice(0, 12)}.bin`;
if (
!Array.isArray(observed.documents) ||
observed.documents.length !== 1 ||
!observed.documents[0].includes("debug")
observed.documents[0] !== expectedDocument
)
throw new Error("Ghidra did not expose exactly one debug Program identity");
throw new Error(
`Ghidra did not expose the digest-bound debug Program identity ${expectedDocument}: ${JSON.stringify(observed.documents)}`,
);
const entry = findValue(observed.procedures, "rea_ghidra_inventory_entry");
const leaf = findValue(observed.procedures, "rea_ghidra_inventory_leaf");
const external = observed.procedures.find(
+59 -4
View File
@@ -2,8 +2,13 @@ import {
parseGhidraFunctionInput,
parseGhidraFunctionResult,
} from "../dist/ghidra/GhidraFunctionValues.js";
import {
parseGhidraInventoryInput,
parseGhidraInventoryResult,
} from "../dist/ghidra/GhidraInventoryValues.js";
import {
assertDenseSwitchDossier,
assertDossier,
assertLocalProcedureInfo,
assertProviderText,
@@ -27,6 +32,20 @@ async function functionCall(client, operation, parameters) {
return parsed.value;
}
async function inventoryCall(client, operation, parameters) {
const input = parseGhidraInventoryInput(operation, parameters);
if (!input.ok) throw input.error;
const called = await client.callTool(operation, input.value);
if (!called.ok)
throw new Error(
`Ghidra ${operation} failed for ${JSON.stringify(input.value)}: ${called.error.message}`,
{ cause: called.error },
);
const parsed = parseGhidraInventoryResult(operation, called.value);
if (!parsed.ok) throw parsed.error;
return parsed.value;
}
export async function verifyDebugFunctionOperations(
client,
{ procedures, strings, entry },
@@ -37,6 +56,14 @@ export async function verifyDebugFunctionOperations(
procedures,
"rea_ghidra_inventory_indirect",
);
const switchProcedure = requireProcedure(
procedures,
"rea_ghidra_inventory_switch",
);
const denseSwitchProcedure = requireProcedure(
procedures,
"rea_ghidra_inventory_dense_switch",
);
const main = requireProcedure(procedures, "main");
const entryString = strings.find(
(item) => item.value === "REA_GHIDRA_INVENTORY_ENTRY",
@@ -88,6 +115,15 @@ export async function verifyDebugFunctionOperations(
);
const branchDossier = await analyzeBranchDossier(client, branch);
const indirectDossier = await analyzeIndirectDossier(client, indirect);
const switchDossier = await analyzeSwitchDossier(client, switchProcedure);
const denseSwitchDossier = await functionCall(client, "analyze_function", {
procedure: denseSwitchProcedure.address,
include_assembly: true,
limit: 500,
max_pseudocode_chars: 100_000,
max_instructions: 5_000,
});
assertDenseSwitchDossier(denseSwitchDossier, denseSwitchProcedure.address);
assertIndirectDossier(indirectDossier, leaf.address);
const { cancellation, timeout } = await verifyRequestCancellationAndTimeout(
@@ -100,6 +136,8 @@ export async function verifyDebugFunctionOperations(
entry: dossierSummary(entryDossier),
branch: dossierSummary(branchDossier),
indirect: dossierSummary(indirectDossier),
switch: dossierSummary(switchDossier),
dense_switch: dossierSummary(denseSwitchDossier),
instruction_window: {
returned: instructionWindow.instructions.returned,
truncated: instructionWindow.instructions.truncated,
@@ -164,7 +202,7 @@ async function resolveXrefOwners(client, entryXrefs) {
for (const address of entryXrefs) {
try {
xrefOwners.push(
await functionCall(client, "resolve_containing_procedure", {
await inventoryCall(client, "resolve_containing_procedure", {
document: null,
address,
}),
@@ -222,6 +260,23 @@ async function analyzeEntryDossier(client, entry, branch, indirect) {
return entryDossier;
}
async function analyzeSwitchDossier(client, switchProcedure) {
const dossier = await functionCall(client, "analyze_function", {
procedure: switchProcedure.address,
include_assembly: true,
limit: 500,
max_pseudocode_chars: 100_000,
max_instructions: 5_000,
});
assertDossier(dossier, {
address: switchProcedure.address,
requireAssembly: true,
requireMultiBlock: true,
requireJumpTable: true,
});
return dossier;
}
async function analyzeBranchDossier(client, branch) {
const branchDossier = await functionCall(client, "analyze_function", {
procedure: branch.address,
@@ -310,7 +365,7 @@ export async function verifyStrippedFunctionOperations(client, entryString) {
});
if (stringXrefs.length === 0)
throw new Error("Ghidra stripped string xref probe is unavailable");
const containing = await functionCall(
const containing = await inventoryCall(
client,
"resolve_containing_procedure",
{
@@ -339,7 +394,7 @@ export async function verifyStrippedFunctionOperations(client, entryString) {
expectedString: "REA_GHIDRA_INVENTORY_ENTRY",
requireAssembly: true,
});
if (symbolTail(dossier.procedure.name) === "rea_ghidra_inventory_entry")
if (matchesSymbol(dossier.procedure.name, "rea_ghidra_inventory_entry"))
throw new Error("Ghidra stripped dossier invented a source symbol");
return dossierSummary(dossier);
}
@@ -424,7 +479,7 @@ export async function verifyCrossFormatOperations({
async function findCrossStringOwner(client, messageXrefs) {
for (const address of messageXrefs) {
const candidate = await functionCall(
const candidate = await inventoryCall(
client,
"resolve_containing_procedure",
{
+53 -3
View File
@@ -126,25 +126,28 @@ try {
format: "elf",
architecture: "x86_64",
});
assertDebugFixture(debug);
const nativeApiCli = await verifyNativeApiCli(debugPath);
const stripped = await verifyTarget(strippedPath, "stripped", {
format: "elf",
architecture: "x86_64",
});
assertStrippedFixture(stripped);
const arm64Elf = await verifyTarget(arm64ElfPath, "cross-arm64-elf", {
format: "elf",
architecture: "arm64",
});
assertCrossFixture(arm64Elf);
const pe = await verifyTarget(pePath, "cross-x86_64-pe", {
format: "pe",
architecture: "x86_64",
});
assertCrossFixture(pe);
const machObject = await verifyTarget(machObjectPath, "cross-x86_64-mach-o", {
format: "mach-o",
architecture: "x86_64",
});
assertDebugFixture(debug);
assertStrippedFixture(stripped);
for (const fixture of [arm64Elf, pe, machObject]) assertCrossFixture(fixture);
assertCrossFixture(machObject);
await assertMalformedFixture(malformedPath);
const customPath = process.env.GHIDRA_TARGET_PATH;
@@ -164,6 +167,7 @@ try {
summary(machObject),
],
malformed_target: "rejected-before-provider-start",
native_api_cli: nativeApiCli,
custom_target: custom === null ? null : summary(custom),
cleanup: "complete",
})}\n`,
@@ -172,6 +176,52 @@ try {
await rm(fixtureRoot, { recursive: true, force: true });
}
async function verifyNativeApiCli(targetPath) {
const { stdout } = await exec(
process.execPath,
[
"scripts/rea.mjs",
"inspect-native-api",
targetPath,
"rea_ghidra_inventory_dense_switch",
"--provider",
"ghidra",
"--json",
],
{
cwd: process.cwd(),
env: process.env,
maxBuffer: 2 * 1024 * 1024,
},
);
const evidence = JSON.parse(stdout);
const boundary = evidence.normalized_result?.boundary;
const truncated = boundary?.jump_tables?.find(
({ mappings_truncated: mappingsTruncated }) => mappingsTruncated === true,
);
const omittedQuestion =
"Which additional data sources or targets were omitted";
if (
evidence.operation !== "inspect_native_api" ||
evidence.provider?.id !== "rea-workflow" ||
evidence.analysis_profile?.provider?.id !== "rea-workflow" ||
boundary?.available !== true ||
truncated?.mappings?.length !== 32 ||
!evidence.normalized_result?.residual_unknowns?.some((question) =>
question.includes(omittedQuestion),
)
)
throw new Error(
`The shipped inspect-native-api CLI did not preserve dense jump-table truncation: ${stdout}`,
);
return {
operation: evidence.operation,
provider: evidence.provider,
mappings_returned: truncated.mappings.length,
residual_unknowns: evidence.normalized_result.residual_unknowns.length,
};
}
async function verifyTarget(targetPath, variant, expectedTarget = null) {
const parsedTarget = await parseBinaryTarget(targetPath);
if (!parsedTarget.ok) throw parsedTarget.error;
+35 -35
View File
@@ -5,7 +5,6 @@ import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
import {
firstProcedureAddress,
requireAddressArray,
@@ -16,8 +15,11 @@ import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProvider.js";
import { REA_WORKFLOW_PROVIDER } from "../dist/application/InvestigationProviders.js";
import { loadRealHopperFixtureTargets } from "./lib/real-hopper-fixture.mjs";
import {
requireBridgeDiagnostic,
requireBridgeProgress,
requireCurrentDocument,
requireSafeDiagnostics,
requireTruthfulMemoryRegions,
verifyRealHopperFixture,
} from "./lib/real-hopper-semantic.mjs";
import {
@@ -99,30 +101,6 @@ const requireOverview = (value, operation) => {
return value;
};
const requireTruthfulMemoryRegions = (segments) => {
if (!Array.isArray(segments) || segments.length === 0) {
throw new Error("list_segments returned no real memory regions");
}
const regions = segments.flatMap((segment) => [
segment,
...(Array.isArray(segment.sections) ? segment.sections : []),
]);
for (const region of regions) {
for (const permission of ["readable", "writable", "executable"]) {
if (![true, false, null].includes(region?.[permission])) {
throw new Error(`list_segments omitted tri-state ${permission}`);
}
}
if (
region.provenance !== "hopper-public-python-api" ||
region.permissions?.available !== false ||
typeof region.permissions.reason !== "string"
) {
throw new Error("list_segments omitted permission provenance");
}
}
};
const verifyRelationships = async (client, options, procedure) => {
const related = {};
for (const operation of ["procedure_callers", "procedure_callees"]) {
@@ -303,16 +281,11 @@ let summary;
try {
await client.connect(transport);
const listed = await client.listTools();
const expectedNames = TOOL_CONTRACTS.map(({ name }) => name).sort();
const actualNames = listed.tools.map(({ name }) => name).sort();
if (JSON.stringify(actualNames) !== JSON.stringify(expectedNames)) {
throw new Error(
"The real server tool inventory did not match its contracts",
);
}
const options = { timeout };
const progressUpdates = [];
const options = {
timeout,
onprogress: (update) => progressUpdates.push(update),
};
const fullSessionStatus = () =>
client.callTool(
{ name: "binary_session", arguments: { detail: "full" } },
@@ -320,6 +293,21 @@ try {
);
const initialSession = await fullSessionStatus();
const initialStatus = requireMcpResult(initialSession, "binary_session");
const listed = await client.listTools();
const availableTools = initialStatus.tool_availability;
if (!Array.isArray(availableTools))
throw new Error("The real session omitted tool availability");
const expectedNames = availableTools
.filter(({ available }) => available === true)
.map(({ name }) => name)
.sort();
const actualNames = listed.tools.map(({ name }) => name).sort();
if (JSON.stringify(actualNames) !== JSON.stringify(expectedNames)) {
throw new Error(
"The real server tool inventory diverged from session availability",
);
}
if (initialStatus.open !== false)
throw new Error("The verifier did not start without a target");
requireHopperSelection(initialStatus, {
@@ -343,6 +331,16 @@ try {
{ name: "list_documents", arguments: {} },
options,
);
requireBridgeProgress(progressUpdates);
const rejectedProcedure = await client.callTool(
{
name: "procedure_info",
arguments: { procedure: "__rea_missing_procedure__" },
},
options,
);
if (rejectedProcedure.isError !== true)
throw new Error("The real Hopper bridge accepted an unknown procedure");
const overview = await client.callTool(
{ name: "binary_overview", arguments: {} },
options,
@@ -435,6 +433,7 @@ try {
);
}
const diagnosticCount = requireSafeDiagnostics(stderrChunks);
requireBridgeDiagnostic(stderrChunks);
const closed = await client.callTool(
{ name: "close_binary", arguments: {} },
@@ -460,6 +459,7 @@ try {
bundledMcpRunning,
stderrBytes,
diagnosticCount,
bridgeProgressUpdates: progressUpdates.length,
dynamicSession: true,
providerBinding,
targets: [targetA, targetB, largeTarget],
+24 -10
View File
@@ -52,7 +52,7 @@ const roots = [
),
];
const policy = {
enabled: true,
status: "enabled",
roots,
nodePath: process.execPath,
bubblewrapPath:
@@ -93,7 +93,7 @@ const authority = new PermissionAuthority(
),
);
const dependencies = {
policy,
policy: () => policy,
host: new SystemJavaScriptReplayHost(),
runner: new LinuxJavaScriptReplayRunner(),
authority,
@@ -131,18 +131,20 @@ const defaultInput = {
{ case_id: "plain", arguments: ["plain"] },
],
generator: { preset: "parser-boundaries", seed: 7, count: 4 },
approved: false,
};
const input =
const suppliedPlanInput =
suppliedInput === undefined
? undefined
: (({ approved: _approved, plan_digest: _planDigest, ...planInput }) =>
planInput)(suppliedInput);
const input =
suppliedPlanInput === undefined
? defaultInput
: {
...suppliedInput,
...suppliedPlanInput,
mode: "plan",
left: suppliedLeft,
...(suppliedRight === undefined ? {} : { right: suppliedRight }),
approved: false,
plan_digest: undefined,
reproducer_export: undefined,
};
@@ -177,7 +179,21 @@ if (
({ status }) => status === "unknown",
) === true
)
throw new Error("Real replay verifier received incomplete Evidence");
throw new Error(
`Real replay verifier received incomplete Evidence: ${JSON.stringify({
authority: evidence?.authority ?? null,
isolation: evidence?.environment?.isolation ?? null,
termination: evidence?.normalized_result?.termination ?? null,
cleanup: evidence?.normalized_result?.cleanup?.state ?? null,
outcomes:
evidence?.normalized_result?.outcomes?.map(({ outcome }) => outcome) ??
[],
stderr: evidence?.normalized_result?.stderr ?? null,
comparison_statuses:
evidence?.normalized_result?.comparison?.map(({ status }) => status) ??
[],
})}`,
);
if (suppliedInput !== undefined) {
process.stdout.write(
@@ -222,7 +238,6 @@ const runSingle = async (path, limits = undefined) => {
},
cases: [{ case_id: "probe", arguments: [] }],
...(limits === undefined ? {} : { limits }),
approved: false,
};
const proposed = await runControlledReplay(dependencies, manifest);
if (!proposed.ok || proposed.value.plan === null)
@@ -288,7 +303,6 @@ const differentialTimeoutInput = {
},
cases: [{ case_id: "probe", arguments: [] }],
limits: { wall_time_ms: 100 },
approved: false,
};
const differentialTimeoutPlan = await runControlledReplay(
dependencies,
+2 -2
View File
@@ -9,7 +9,7 @@ import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { verifyAvailableToolCatalog } from "./lib/verify-package-core.mjs";
import { verifyCompleteToolCatalog } from "./lib/verify-package-core.mjs";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
const exec = promisify(execFile);
@@ -84,7 +84,7 @@ try {
let toolCount = 0;
try {
await client.connect(server);
toolCount = (await verifyAvailableToolCatalog(client)).length;
toolCount = (await verifyCompleteToolCatalog(client)).length;
} finally {
await client.close();
}
+30
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.1.0",
"packages": [
{
"registryType": "npm",
"registryBaseUrl": "https://registry.npmjs.org",
"identifier": "rea-agents",
"version": "3.1.0",
"runtimeHint": "npx",
"transport": {
"type": "stdio"
},
"packageArguments": [
{
"type": "positional",
"value": "mcp"
}
]
}
]
}
+10 -3
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: 110
catalog_digest: "e8cac51f2f17a5b9a088c3ff773473c4897823401481fc0875a28fe7daf8eee0"
tool_count: 116
catalog_digest: "bab5657d9525ec4f798d0a9ef6001412d9c11f77fb0ddc925c7076fd10921dc9"
---
# REA
@@ -23,7 +23,8 @@ Choose the first tool from the target the user supplied. Do not call
- ASAR or extracted JavaScript/Electron tree:
`analyze_javascript_application`.
- Archive, application package, ZIP/APK/IPA/MSIX/AppX, or DMG:
`inventory_artifact`.
`open_binary` with the supplied local path, then `inspect_artifact` or
`inventory_artifact` (both operate on the active target and accept no path).
- Managed PE/CLI assembly: `inspect_managed_artifact`.
- User-owned browser page already open: `list_browser_targets`.
- User-owned Electron runtime already open: `list_electron_targets`.
@@ -34,6 +35,12 @@ If the app is missing, ask which app to inspect. Resolve a human-readable app
name to one clear installed artifact when possible; ask only when matches are
ambiguous. Never choose an example app on the user's behalf.
In a target-free session, use `open_binary` to bind any archive/package or
native target whose analysis tool operates on the active target. Do not call a
tool hidden from `tools/list`; inspect `binary_session` with
`detail: "capabilities"` for the exact remediation when a desired capability
is unavailable.
## Work summary-first
Start with the default summary projection. Do not repeat an identical tool call.
+229
View File
@@ -0,0 +1,229 @@
import type {
AnalysisExecution,
AnalysisOperation,
AnalysisOperationPort,
} from "./AnalysisProvider.js";
import { AnalysisOutputError, type AnalysisError } from "../domain/errors.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js";
type Facet =
| { readonly state: "available"; readonly value: JsonValue }
| {
readonly state: "unavailable";
readonly reason: string;
readonly remediation: string;
};
const parameters = (
document: string | undefined,
address?: string,
): Readonly<Record<string, JsonValue>> => ({
...(document === undefined ? {} : { document }),
...(address === undefined ? {} : { address }),
});
const executionOptions = (
signal: AbortSignal | undefined,
): { readonly signal?: AbortSignal } =>
signal === undefined ? {} : { signal };
/** Compose the provider's volatile selection state into one coherent query. */
export const getNavigationContext = async (
analysis: AnalysisOperationPort,
input: { readonly document?: string | undefined },
signal?: AbortSignal,
): Promise<Result<JsonValue, AnalysisError>> => {
const document =
input.document ??
(await analysis.execute("current_document", {}, executionOptions(signal)));
if (typeof document !== "string" && !document.ok) return document;
const documentName =
typeof document === "string" ? document : document.value.result;
const documentArgs =
typeof documentName === "string" ? parameters(documentName) : {};
const address = await analysis.execute(
"current_address",
documentArgs,
executionOptions(signal),
);
if (!address.ok) return address;
if (typeof address.value.result !== "string")
return ok({
document: documentName,
address: address.value.result,
procedure: null,
});
const procedure = await analysis.execute(
"resolve_containing_procedure",
parameters(
typeof documentName === "string" ? documentName : undefined,
address.value.result,
),
executionOptions(signal),
);
if (!procedure.ok) {
if (!isMissingCurrentProcedure(procedure.error)) return procedure;
return ok({
document: documentName,
address: address.value.result,
procedure: null,
});
}
return ok({
document: documentName,
address: address.value.result,
procedure: containingProcedureName(procedure.value.result),
});
};
/** Inspect bounded provider-neutral facets at one explicit reproducible address. */
export const inspectAddressContext = async (
analysis: AnalysisOperationPort,
input: { readonly address: string; readonly document?: string | undefined },
signal?: AbortSignal,
): Promise<Result<JsonValue, AnalysisError>> => {
const document = await resolveDocument(analysis, input.document, signal);
if (!document.ok) return document;
const args = parameters(document.value, input.address);
const operations = [
"address_name",
"resolve_containing_procedure",
"comment",
"inline_comment",
"list_bookmarks",
] as const satisfies readonly AnalysisOperation[];
const results = await Promise.all(
operations.map((operation) =>
analysis.execute(
operation,
operation === "list_bookmarks" ? parameters(document.value) : args,
executionOptions(signal),
),
),
);
for (const result of results) {
if (!result.ok && isRequestLevelFailure(result.error)) return result;
}
const name = facet(results[0], operations[0]);
const procedure = facet(results[1], operations[1]);
const comment = facet(results[2], operations[2]);
const inlineComment = facet(results[3], operations[3]);
const bookmarks = facet(results[4], operations[4]);
return ok({
address: input.address,
document: document.value,
name,
procedure,
comment,
inline_comment: inlineComment,
bookmarks:
bookmarks.state === "available"
? matchingBookmarks(
bookmarks.value,
resolvedAddress(results[1], input.address),
)
: bookmarks,
});
};
const resolveDocument = async (
analysis: AnalysisOperationPort,
document: string | undefined,
signal: AbortSignal | undefined,
): Promise<Result<string, AnalysisError>> => {
if (document !== undefined) return ok(document);
const current = await analysis.execute(
"current_document",
{},
executionOptions(signal),
);
if (!current.ok) return current;
return typeof current.value.result === "string"
? ok(current.value.result)
: err(
new AnalysisOutputError(
"current_document",
"expected a document identity string",
),
);
};
const facet = (
result: Result<AnalysisExecution, AnalysisError> | undefined,
operation: AnalysisOperation,
): Facet =>
result?.ok === true
? { state: "available", value: result.value.result }
: {
state: "unavailable",
reason: result?.error.message ?? `${operation} returned no result`,
remediation: `Call ${operation} directly to inspect provider availability and diagnostics.`,
};
const isMissingCurrentProcedure = (error: AnalysisError): boolean =>
/(?:no procedure exists|not in a procedure)/iu.test(error.message);
const isRequestLevelFailure = (error: AnalysisError): boolean =>
error._tag === "AnalysisCancelledError" ||
error._tag === "AnalysisTimeoutError" ||
error._tag === "NoBinaryOpenError" ||
error._tag === "PermissionRequiredError";
const containingProcedureName = (value: JsonValue): string | null => {
if (typeof value !== "object" || value === null || Array.isArray(value))
return null;
if (value.found !== true) return null;
const procedure = value.procedure;
if (
typeof procedure !== "object" ||
procedure === null ||
Array.isArray(procedure)
)
return null;
return typeof procedure.name === "string" ? procedure.name : null;
};
const resolvedAddress = (
result: Result<AnalysisExecution, AnalysisError> | undefined,
fallback: string,
): string => {
if (result?.ok !== true) return fallback;
const value = result.value.result;
if (typeof value !== "object" || value === null || Array.isArray(value))
return fallback;
return typeof value.query_address === "string"
? value.query_address
: fallback;
};
const matchingBookmarks = (value: JsonValue, address: string): Facet => {
if (!Array.isArray(value))
return {
state: "unavailable",
reason: "list_bookmarks returned malformed output; expected an array.",
remediation:
"Retry list_bookmarks directly and report the provider output shape if it remains malformed.",
};
if (!value.every(isBookmark))
return {
state: "unavailable",
reason:
"list_bookmarks returned malformed entries; expected address and name strings.",
remediation:
"Retry list_bookmarks directly and report the provider output shape if it remains malformed.",
};
return {
state: "available",
value: value.filter((bookmark) => bookmark.address === address),
};
};
const isBookmark = (
value: JsonValue,
): value is { readonly address: string; readonly name: string } =>
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
typeof value.address === "string" &&
typeof value.name === "string";
+10 -8
View File
@@ -26,7 +26,7 @@ import type {
AnalysisOperation,
CapabilityDescriptor,
} from "./AnalysisProvider.js";
import { OFFICIAL_TOOL_CONTRACTS } from "../contracts/toolContracts.js";
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
const STATEFUL_OPERATIONS: ReadonlySet<AnalysisOperation> = new Set([
"health",
@@ -47,13 +47,15 @@ const CURSOR_DEFAULT_OPERATIONS: ReadonlySet<AnalysisOperation> = new Set([
"xrefs",
]);
const DOCUMENT_SCOPED_OPERATIONS: ReadonlySet<AnalysisOperation> = new Set(
OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name).filter(
(name) =>
name !== "current_document" &&
name !== "list_documents" &&
name !== "set_current_document",
),
const DOCUMENT_SCOPED_OPERATIONS: ReadonlySet<string> = new Set(
GENERATED_MCP_TOOL_CATALOG.filter(({ kind }) => kind === "official-proxy")
.map(({ name }) => name)
.filter(
(name) =>
name !== "current_document" &&
name !== "list_documents" &&
name !== "set_current_document",
),
);
/** Whether an operation is immutable and independent of provider UI state. */
@@ -12,6 +12,7 @@ import {
type TraceApplicationFeatureRequest,
type TraceJavaScriptSemanticsRequest,
} from "../contracts/applicationWorkflowInputContracts.js";
import type { Evidence } from "../domain/evidence.js";
import { AnalysisInputError, type AnalysisError } from "../domain/errors.js";
import { projectInputIssues } from "../domain/inputIssueProjection.js";
import { compareApplicationVersionsInputSchema } from "../domain/javascriptApplicationVersionComparisonSchemas.js";
@@ -69,11 +70,7 @@ export const resolveTraceApplicationFeatureRequestValidated = (
input: TraceApplicationFeatureRequest,
lookup?: EvidenceLookup,
): Result<TraceInput, AnalysisError> => {
const application = graphEvidence(
input.application,
input.application_evidence_id,
lookup,
);
const application = graphEvidence(input, lookup);
if (!application.ok) return application;
const native = resolveEvidenceReferences(
lookup,
@@ -109,11 +106,7 @@ export const resolveTraceJavaScriptSemanticsRequestValidated = (
input: TraceJavaScriptSemanticsRequest,
lookup?: EvidenceLookup,
): Result<SemanticTraceInput, AnalysisError> => {
const application = graphEvidence(
input.application,
input.application_evidence_id,
lookup,
);
const application = graphEvidence(input, lookup);
if (!application.ok) return application;
const raw = { application: application.value, query: input.query };
const parsed = traceJavaScriptSemanticsInputSchema.safeParse(raw);
@@ -138,9 +131,19 @@ export const resolveCompareApplicationVersionsRequestValidated = (
input: CompareApplicationVersionsRequest,
lookup?: EvidenceLookup,
): Result<ComparisonInput, AnalysisError> => {
const left = graphEvidence(input.left, input.left_evidence_id, lookup);
const left = graphEvidence(
"left" in input
? { application: input.left }
: { application_evidence_id: input.left_evidence_id },
lookup,
);
if (!left.ok) return left;
const right = graphEvidence(input.right, input.right_evidence_id, lookup);
const right = graphEvidence(
"right" in input
? { application: input.right }
: { application_evidence_id: input.right_evidence_id },
lookup,
);
if (!right.ok) return right;
const leftNative = resolveEvidenceReferences(
lookup,
@@ -190,11 +193,7 @@ export const resolveCompareSourceToBundleRequestValidated = (
input: CompareSourceToBundleRequest,
lookup?: EvidenceLookup,
): Result<SourceToBundleInput, AnalysisError> => {
const application = graphEvidence(
input.application,
input.application_evidence_id,
lookup,
);
const application = graphEvidence(input, lookup);
if (!application.ok) return application;
const raw = {
reference: input.reference,
@@ -226,9 +225,19 @@ export const resolveCompareJavaScriptExportShapesRequestValidated = (
input: CompareJavaScriptExportShapesRequest,
lookup?: EvidenceLookup,
): Result<ExportShapeComparisonInput, AnalysisError> => {
const left = graphEvidence(input.left, input.left_evidence_id, lookup);
const left = graphEvidence(
"left" in input
? { application: input.left }
: { application_evidence_id: input.left_evidence_id },
lookup,
);
if (!left.ok) return left;
const right = graphEvidence(input.right, input.right_evidence_id, lookup);
const right = graphEvidence(
"right" in input
? { application: input.right }
: { application_evidence_id: input.right_evidence_id },
lookup,
);
if (!right.ok) return right;
const raw = {
left: left.value,
@@ -249,19 +258,15 @@ export const resolveCompareJavaScriptExportShapesRequestValidated = (
};
const graphEvidence = (
evidence:
| TraceApplicationFeatureRequest["application"]
| TraceJavaScriptSemanticsRequest["application"]
| CompareApplicationVersionsRequest["left"]
| CompareJavaScriptExportShapesRequest["left"]
| CompareSourceToBundleRequest["application"],
evidenceId: string | undefined,
source:
| { readonly application: Evidence }
| { readonly application_evidence_id: string },
lookup: EvidenceLookup | undefined,
) => {
if (evidence !== undefined) return ok(evidence);
if ("application" in source) return ok(source.application);
const resolved = resolveEvidenceReferences(
lookup,
evidenceId === undefined ? [] : [evidenceId],
[source.application_evidence_id],
APPLICATION_GRAPH_IDENTITIES,
);
return resolved.ok ? ok(resolved.value[0]) : resolved;
@@ -0,0 +1,41 @@
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import type {
ArtifactIntegrityPolicy,
ArtifactNativeMountPolicy,
} from "./types.js";
/** Parsed caller intent for integrity mismatch handling. */
export type ArtifactIntegrityIntent =
| { readonly mode: "fail" }
| {
readonly mode: "record-and-continue";
readonly maxMismatches: number;
};
/** Resolve caller approval and operator policy before native reader selection. */
export const resolveNativeMountPolicy = (
approved: boolean,
enabled: boolean,
): ArtifactNativeMountPolicy => {
if (!approved) return { status: "disabled" };
if (!enabled)
throw new ArtifactReaderFailure(
"unavailable",
"Native DMG mounting is disabled by operator policy",
);
return { status: "approved" };
};
/** Resolve parsed caller intent and operator policy before artifact scanning. */
export const resolveArtifactIntegrityPolicy = (
intent: ArtifactIntegrityIntent,
enabled: boolean,
): ArtifactIntegrityPolicy => {
if (intent.mode === "fail") return intent;
if (!enabled)
throw new ArtifactReaderFailure(
"policy",
"Integrity continuation requires explicit approval and operator policy",
);
return intent;
};
+2 -10
View File
@@ -1,8 +1,5 @@
import { AsarArtifactReader } from "../../artifacts/AsarArtifactReader.js";
import {
ArtifactReaderFailure,
type ArtifactReader,
} from "../../artifacts/ArtifactReader.js";
import type { ArtifactReader } from "../../artifacts/ArtifactReader.js";
import { DirectoryArtifactReader } from "../../artifacts/DirectoryArtifactReader.js";
import { MachOSliceArtifactReader } from "../../artifacts/MachOSliceArtifactReader.js";
import { NativeDmgArtifactReader } from "../../artifacts/NativeDmgArtifactReader.js";
@@ -32,12 +29,7 @@ export const createReader = async (
? new MachOSliceArtifactReader(path)
: undefined;
case "dmg":
if (!nativeMount.nativeMountApproved) return undefined;
if (!nativeMount.nativeMountEnabled)
throw new ArtifactReaderFailure(
"unavailable",
"Native DMG mounting is disabled by operator policy",
);
if (nativeMount.status === "disabled") return undefined;
return NativeDmgArtifactReader.create(path, signal);
default:
return undefined;
@@ -44,14 +44,6 @@ export const scanCanonicalArtifactInventory = async (
options: ArtifactInventoryOptions = {},
): Promise<ArtifactInventorySnapshot> => {
const integrity = options.integrity ?? STRICT_INTEGRITY_POLICY;
if (
integrity.mode === "record-and-continue" &&
(!integrity.approved || !integrity.enabled)
)
throw new ArtifactReaderFailure(
"unavailable",
"Integrity continuation requires explicit approval and operator policy",
);
const metadata = await lstat(path);
const rootFormat = await classifyRoot(path, metadata.isDirectory());
const rootDigest = metadata.isDirectory()
+12 -12
View File
@@ -166,19 +166,19 @@ const digestArtifactEntry = async (
context.totalBytes += digest.bytes;
const mismatched =
entry.declaredSha256 !== null && entry.declaredSha256 !== digest.sha256;
if (mismatched && context.integrity.mode === "fail")
throw new ArtifactReaderFailure(
"integrity",
`Artifact integrity metadata disagrees with content: ${logicalPath}`,
undefined,
{
logicalPath,
declaredSha256: entry.declaredSha256,
calculatedSha256: digest.sha256,
unpacked: entry.unpacked,
},
);
if (mismatched && entry.declaredSha256 !== null) {
if (context.integrity.mode === "fail")
throw new ArtifactReaderFailure(
"integrity",
`Artifact integrity metadata disagrees with content: ${logicalPath}`,
undefined,
{
logicalPath,
declaredSha256: entry.declaredSha256,
calculatedSha256: digest.sha256,
unpacked: entry.unpacked,
},
);
if (context.pendingContradictions.length >= context.integrity.maxMismatches)
throw new ArtifactReaderFailure(
"limit",
+12 -17
View File
@@ -13,30 +13,25 @@ export interface InventoryPageInput {
readonly edgeLimit: number;
}
/** Per-request authority combined with operator-owned native mount policy. */
export interface ArtifactNativeMountPolicy {
readonly nativeMountApproved: boolean;
readonly nativeMountEnabled: boolean;
}
/** Resolved native mount authority admitted to the artifact reader. */
export type ArtifactNativeMountPolicy =
| { readonly status: "disabled" }
| { readonly status: "approved" };
export const NATIVE_MOUNT_DISABLED: ArtifactNativeMountPolicy = {
nativeMountApproved: false,
nativeMountEnabled: false,
status: "disabled",
};
/** Explicit caller approval bounded by operator-owned integrity policy. */
export interface ArtifactIntegrityPolicy {
readonly mode: "fail" | "record-and-continue";
readonly approved: boolean;
readonly enabled: boolean;
readonly maxMismatches: number;
}
/** Resolved integrity behavior admitted to the artifact scanner. */
export type ArtifactIntegrityPolicy =
| { readonly mode: "fail" }
| {
readonly mode: "record-and-continue";
readonly maxMismatches: number;
};
export const STRICT_INTEGRITY_POLICY: ArtifactIntegrityPolicy = {
mode: "fail",
approved: false,
enabled: false,
maxMismatches: 1,
};
/** Options shared by artifact inventory scans. */
+27 -24
View File
@@ -24,8 +24,7 @@ import type {
ProviderIdentity,
} from "./AnalysisProvider.js";
import type { AnalysisOperation } from "./AnalysisProvider.js";
import { enhancedToolNameSchema } from "../contracts/enhancedInputs.js";
import { OFFICIAL_TOOL_CONTRACTS } from "../contracts/toolContracts.js";
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
import type { BinarySessionPort } from "./BinarySessionPort.js";
import {
SessionProviderRouter,
@@ -41,7 +40,14 @@ import { prepareSessionExecution } from "./BinarySessionExecution.js";
import { closeAnalysisClient } from "./AnalysisClientCleanup.js";
export type { BinarySessionPort } from "./BinarySessionPort.js";
const OFFICIAL_OPERATIONS: ReadonlySet<string> = new Set(
OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name),
GENERATED_MCP_TOOL_CATALOG.filter(
({ kind }) => kind === "official-proxy",
).map(({ name }) => name),
);
const ENHANCED_OPERATIONS: ReadonlySet<string> = new Set(
GENERATED_MCP_TOOL_CATALOG.filter(({ kind }) => kind === "enhanced").map(
({ name }) => name,
),
);
/**
@@ -68,9 +74,9 @@ export class BinarySession
#transition: Promise<void> = Promise.resolve();
readonly #calls = new Set<Promise<unknown>>();
readonly #providerRouter: SessionProviderRouter;
readonly #runtimeAvailability = new Map<
readonly #runtimeUnavailability = new Map<
string,
{ readonly available: boolean; readonly reason: string | null }
{ readonly reason: string }
>();
readonly #availabilityListeners = new Set<() => void | Promise<void>>();
@@ -100,7 +106,7 @@ export class BinarySession
if (operation !== undefined) {
const exact = route.capabilities?.get(operation)?.provider;
if (exact !== undefined) selected = exact;
else if (enhancedToolNameSchema.safeParse(operation).success) {
else if (ENHANCED_OPERATIONS.has(operation)) {
const providers = new Map<string, ProviderIdentity>();
for (const descriptor of route.capabilities?.values() ?? [])
if (
@@ -183,6 +189,7 @@ export class BinarySession
const imported = this.importAnalysisSnapshot(options.snapshot);
if (!imported.ok) return imported;
}
this.resetSnapshotInvalidation();
return ok(target);
}
await this.#drainCalls();
@@ -265,7 +272,7 @@ export class BinarySession
target: this.#active?.target,
route: this.#currentRoute(),
router: this.#providerRouter,
runtimeAvailability: this.#runtimeAvailability,
runtimeUnavailability: this.#runtimeUnavailability,
runId: this.#active?.runId,
runtimeLineageSnapshots:
this.#active?.client.runtimeLineageSnapshots?.() ?? [],
@@ -337,12 +344,12 @@ export class BinarySession
result: Result<AnalysisExecution, AnalysisError>,
): void {
if (result.ok) {
if (this.#setRuntimeAvailability(operation, true, null))
if (this.#markRuntimeAvailable(operation))
this.#emitAvailabilityChanged();
return;
}
if (result.error._tag === "AnalysisCapabilityUnavailableError") {
if (this.#setRuntimeAvailability(operation, false, result.error.message))
if (this.#markRuntimeUnavailable(operation, result.error.message))
this.#emitAvailabilityChanged();
return;
}
@@ -360,32 +367,28 @@ export class BinarySession
for (const descriptor of capabilities?.values() ?? [])
if (providerId !== undefined && descriptor.provider.id === providerId)
changed =
this.#setRuntimeAvailability(
this.#markRuntimeUnavailable(
descriptor.operation,
false,
"Provider became unavailable during this session.",
) || changed;
if (changed) this.#emitAvailabilityChanged();
}
}
#setRuntimeAvailability(
operation: string,
available: boolean,
reason: string | null,
): boolean {
const current = this.#runtimeAvailability.get(operation);
if (current?.available === available && current.reason === reason)
return false;
if (available && current === undefined) return false;
if (available) this.#runtimeAvailability.delete(operation);
else this.#runtimeAvailability.set(operation, { available, reason });
#markRuntimeAvailable(operation: string): boolean {
return this.#runtimeUnavailability.delete(operation);
}
#markRuntimeUnavailable(operation: string, reason: string): boolean {
const current = this.#runtimeUnavailability.get(operation);
if (current?.reason === reason) return false;
this.#runtimeUnavailability.set(operation, { reason });
return true;
}
#clearRuntimeAvailability(): void {
if (this.#runtimeAvailability.size === 0) return;
this.#runtimeAvailability.clear();
if (this.#runtimeUnavailability.size === 0) return;
this.#runtimeUnavailability.clear();
this.#emitAvailabilityChanged();
}
@@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";
import type {
AnalysisClient,
AnalysisProvider,
ProviderIdentity,
} from "./AnalysisProvider.js";
import { AnalysisProviderRegistry } from "./AnalysisProviderRegistry.js";
import { composeBinarySession } from "./BinarySessionComposition.js";
const auxiliaryProvider = (identity: ProviderIdentity): AnalysisProvider => ({
identity: () => identity,
capabilities: () => [],
createClient: (): AnalysisClient => {
throw new Error("The composition check must not create provider clients");
},
});
describe("composeBinarySession", () => {
it("exposes the complete configured provider set without starting clients", () => {
const session = composeBinarySession(new AnalysisProviderRegistry([]), [
auxiliaryProvider({
id: "test-auxiliary",
name: "Test auxiliary provider",
version: "1.0.0",
}),
]);
expect(session.providerIdentity()).toEqual({
id: "composite:test-auxiliary",
name: "REA composite analysis provider",
version: null,
});
});
});
@@ -0,0 +1,62 @@
import type {
AnalysisClientFactory,
AnalysisProfileResolution,
AnalysisProfileResolutionOptions,
AnalysisProvider,
} from "./AnalysisProvider.js";
import { AnalysisProviderRegistry } from "./AnalysisProviderRegistry.js";
import { BinarySession } from "./BinarySession.js";
import { SessionProviderRouter } from "./SessionProviderRouter.js";
import type { BinaryTarget } from "../domain/binaryTarget.js";
import type { AnalysisError } from "../domain/errors.js";
import type { Result } from "../domain/result.js";
type BinarySessionOptions = {
readonly resolveAnalysisProfile?: (
target: BinaryTarget,
options?: AnalysisProfileResolutionOptions,
) => Promise<Result<AnalysisProfileResolution, AnalysisError>>;
};
type BinarySessionOptionsOrResolver =
| BinarySessionOptions
| BinarySessionOptions["resolveAnalysisProfile"];
const normalizeBinarySessionOptions = (
optionsOrResolver?: BinarySessionOptionsOrResolver,
): BinarySessionOptions =>
typeof optionsOrResolver === "function"
? { resolveAnalysisProfile: optionsOrResolver }
: (optionsOrResolver ?? {});
/**
* Compose one provider-neutral session from deep-provider selection and
* disjoint auxiliary operation families.
*/
export const composeBinarySession = (
registry: AnalysisProviderRegistry | SessionProviderRouter,
auxiliaryProviders: readonly AnalysisProvider[] = [],
): BinarySession =>
new BinarySession(
registry instanceof SessionProviderRouter
? registry
: SessionProviderRouter.selectable(registry, auxiliaryProviders),
);
/**
* Compose a session through the same internal seam for focused adapter tests.
* The constructor remains private to this composition module, so callers test
* provider behavior without coupling to BinarySession construction.
*/
export const composeBinarySessionFromFactory = (
provider: AnalysisProvider | AnalysisClientFactory | SessionProviderRouter,
optionsOrResolver?: BinarySessionOptionsOrResolver,
): BinarySession =>
new BinarySession(provider, normalizeBinarySessionOptions(optionsOrResolver));
/** Compose a focused session from any provider-shaped test seam. */
export const composeBinarySessionFromProvider = (
provider: AnalysisProvider | AnalysisClientFactory | SessionProviderRouter,
optionsOrResolver?: BinarySessionOptionsOrResolver,
): BinarySession =>
new BinarySession(provider, normalizeBinarySessionOptions(optionsOrResolver));
+16
View File
@@ -27,6 +27,18 @@ import type {
ProviderIdentity,
} from "./AnalysisProvider.js";
/** Compact identity and summary for one retained immutable Evidence bundle. */
export interface EvidenceBundleSnapshot {
readonly bundleDigest: string;
readonly bundleVersion: 2;
readonly bytes: number;
readonly records: number;
readonly unknowns: number;
readonly scope: "session";
readonly survivesSession: false;
readonly uri: string;
}
/** Target lifecycle used by CLI and MCP without exposing a concrete provider. */
export interface BinarySessionPort extends AnalysisOperationPort {
open(
@@ -49,6 +61,9 @@ export interface BinarySessionPort extends AnalysisOperationPort {
hasEvidence(evidenceId: string): boolean;
evidenceById(evidenceId: string): Evidence | undefined;
exportEvidenceBundle(): EvidenceBundle;
snapshotEvidenceBundle(): Result<EvidenceBundleSnapshot, EvidenceLimitError>;
releaseEvidenceBundle(bundleDigest: string): boolean;
retainedEvidenceBundle(digest: string): string | undefined;
importEvidenceBundle(
bundle: unknown,
): Result<number, EvidenceIntegrityError | EvidenceLimitError>;
@@ -101,4 +116,5 @@ export interface BinarySessionPort extends AnalysisOperationPort {
): AnalysisProfileCommitment | undefined;
openCompatibility(): Readonly<Record<string, JsonValue>>;
onAvailabilityChanged?(listener: () => void | Promise<void>): () => void;
onAnalysisSnapshotChanged?(listener: () => void | Promise<void>): () => void;
}
+119 -10
View File
@@ -3,11 +3,14 @@ import type { AnalysisSnapshot } from "../domain/analysisSnapshot.js";
import type { BinaryTarget } from "../domain/binaryTarget.js";
import { createEvidence, type Evidence } from "../domain/evidence.js";
import type { EvidenceBundle } from "../domain/evidenceBundle.js";
import { evidenceBundleForTarget } from "../domain/evidenceBundle.js";
import {
evidenceBundleForTarget,
serializeEvidenceBundle,
} from "../domain/evidenceBundle.js";
import {
EvidenceIntegrityError,
EvidenceLimitError,
type AnalysisError,
type EvidenceLimitError,
type UnknownRegistryError,
} from "../domain/errors.js";
import {
@@ -24,7 +27,8 @@ import type {
UnknownStatus,
UpdateUnknownInput,
} from "../domain/residualUnknown.js";
import { err, type Result } from "../domain/result.js";
import { err, ok, type Result } from "../domain/result.js";
import type { EvidenceBundleSnapshot } from "./BinarySessionPort.js";
import type {
AnalysisExecution,
AnalysisOperation,
@@ -44,17 +48,28 @@ export interface ActiveAnalysisBinding {
/** Owns session evidence, snapshots, workspaces, and residual unknowns. */
export abstract class BinarySessionRecords {
static readonly MAX_RETAINED_BUNDLES = 16;
static readonly MAX_RETAINED_BUNDLE_BYTES = 64 * 1024 * 1024;
readonly #evidence = new EvidenceLedger({
maxRecords: 10_000,
maxBytes: 64 * 1024 * 1024,
});
readonly #snapshot = new AnalysisSnapshotCache();
readonly #retainedBundles = new Map<string, string>();
#retainedBundleBytes = 0;
readonly #investigationWorkspaces = new Map<string, InvestigationWorkspace>();
readonly #coverageWorkspaces = new Map<
string,
ReconstructionCoverageWorkspace
>();
#snapshotInvalidated = false;
readonly #snapshotListeners = new Set<() => void | Promise<void>>();
/** Observe changes to the mutable current analysis snapshot resource. */
onAnalysisSnapshotChanged(listener: () => void | Promise<void>): () => void {
this.#snapshotListeners.add(listener);
return () => this.#snapshotListeners.delete(listener);
}
recordEvidence(
evidence: Evidence,
@@ -62,7 +77,9 @@ export abstract class BinarySessionRecords {
"added" | "duplicate",
EvidenceIntegrityError | EvidenceLimitError
> {
return this.#evidence.record(evidence);
const recorded = this.#evidence.record(evidence);
if (recorded.ok && recorded.value === "added") this.#emitSnapshotChanged();
return recorded;
}
hasEvidence(evidenceId: string): boolean {
@@ -77,10 +94,68 @@ export abstract class BinarySessionRecords {
return this.#evidence.export();
}
snapshotEvidenceBundle(): Result<EvidenceBundleSnapshot, EvidenceLimitError> {
const bundle = this.#evidence.export();
const encoded = serializeEvidenceBundle(bundle);
const digest = createHash("sha256").update(encoded).digest("hex");
const existing = this.#retainedBundles.get(digest);
if (existing === undefined) {
if (
this.#retainedBundles.size >= BinarySessionRecords.MAX_RETAINED_BUNDLES
)
return err(
new EvidenceLimitError(
"records",
BinarySessionRecords.MAX_RETAINED_BUNDLES,
),
);
const bytes = Buffer.byteLength(encoded);
if (
this.#retainedBundleBytes + bytes >
BinarySessionRecords.MAX_RETAINED_BUNDLE_BYTES
)
return err(
new EvidenceLimitError(
"bytes",
BinarySessionRecords.MAX_RETAINED_BUNDLE_BYTES,
),
);
this.#retainedBundles.set(digest, encoded);
this.#retainedBundleBytes += bytes;
}
return ok({
bundleDigest: digest,
bundleVersion: 2,
bytes: Buffer.byteLength(existing ?? encoded),
records: bundle.records.length,
unknowns: bundle.unknowns.length,
scope: "session",
survivesSession: false,
uri: `rea://evidence-bundle/${digest}`,
});
}
retainedEvidenceBundle(digest: string): string | undefined {
return this.#retainedBundles.get(digest);
}
/** Release one immutable bundle so bounded retention has an explicit recovery path. */
releaseEvidenceBundle(bundleDigest: string): boolean {
const encoded = this.#retainedBundles.get(bundleDigest);
if (encoded === undefined) return false;
this.#retainedBundles.delete(bundleDigest);
this.#retainedBundleBytes -= Buffer.byteLength(encoded);
this.#emitSnapshotChanged();
return true;
}
importEvidenceBundle(
bundle: unknown,
): Result<number, EvidenceIntegrityError | EvidenceLimitError> {
return this.#evidence.import(bundle);
const imported = this.#evidence.import(bundle);
if (!imported.ok) return imported;
if (imported.value.changed) this.#emitSnapshotChanged();
return ok(imported.value.recordsAdded);
}
protected abstract activeAnalysisBinding(): ActiveAnalysisBinding | undefined;
@@ -120,13 +195,18 @@ export abstract class BinarySessionRecords {
"Analysis snapshot profile_mismatch: the active target has no concrete analysis profile",
),
);
return this.#snapshot.import(
const imported = this.#snapshot.import(
snapshot,
active === undefined
? undefined
: { target: active.target, profile: active.profile },
(bundle) => this.#evidence.import(bundle),
(bundle) => {
const imported = this.#evidence.import(bundle);
return imported.ok ? ok(imported.value.recordsAdded) : imported;
},
);
if (imported.ok) this.#emitSnapshotChanged();
return imported;
}
protected matchesSnapshot(
@@ -141,6 +221,7 @@ export abstract class BinarySessionRecords {
profile: AnalysisProfileCommitment,
): void {
this.#snapshot.select(target, profile);
this.#emitSnapshotChanged();
}
protected lookupSnapshot(
@@ -158,25 +239,46 @@ export abstract class BinarySessionRecords {
input: Parameters<AnalysisSnapshotCache["record"]>[0],
): void {
this.#snapshot.record(input);
this.#emitSnapshotChanged();
}
protected invalidateSnapshot(): void {
this.#snapshot.clear();
this.#snapshotInvalidated = true;
this.#emitSnapshotChanged();
}
protected resetSnapshotInvalidation(): void {
if (!this.#snapshotInvalidated) return;
this.#snapshotInvalidated = false;
this.#emitSnapshotChanged();
}
protected clearSnapshot(): void {
this.#snapshot.clear();
this.#emitSnapshotChanged();
}
protected clearSessionRecords(): void {
this.#evidence.clear();
this.#snapshot.clear();
this.#snapshotInvalidated = false;
this.#retainedBundles.clear();
this.#retainedBundleBytes = 0;
this.#emitSnapshotChanged();
}
#emitSnapshotChanged(): void {
for (const listener of this.#snapshotListeners) {
try {
const notification = listener();
if (notification !== undefined)
void notification.catch(() => undefined);
} catch {
// External resource observers are best-effort; one callback must not
// make a committed evidence mutation appear to fail.
}
}
}
retainInvestigationWorkspace(
@@ -231,21 +333,25 @@ export abstract class BinarySessionRecords {
input: RecordUnknownInput,
): Result<ResidualUnknown, AnalysisError> {
const target = this.activeAnalysisBinding()?.target;
return this.#evidence.recordUnknown(
const recorded = this.#evidence.recordUnknown(
input,
unknownMutationEvidence(target, input),
);
if (recorded.ok) this.#emitSnapshotChanged();
return recorded;
}
recordEvidenceWithUnknown(
evidence: Evidence,
input: RecordUnknownInput,
): Result<ResidualUnknown | null, AnalysisError> {
return this.#evidence.recordWithUnknown(
const recorded = this.#evidence.recordWithUnknown(
evidence,
input,
unknownMutationEvidence(undefined, input),
);
if (recorded.ok) this.#emitSnapshotChanged();
return recorded;
}
updateUnknown(
@@ -267,7 +373,9 @@ export abstract class BinarySessionRecords {
"Registry mutation evidence records analyst intent, not proof of the answer.",
],
});
return this.#evidence.updateUnknown(input, evidence);
const updated = this.#evidence.updateUnknown(input, evidence);
if (updated.ok) this.#emitSnapshotChanged();
return updated;
}
listUnknowns(
@@ -307,3 +415,4 @@ const sortedWorkspaces = <
left.revision - right.revision,
)
.map((workspace) => structuredClone(workspace));
import { createHash } from "node:crypto";
+13 -7
View File
@@ -14,9 +14,9 @@ interface BinarySessionStatusInput {
readonly target: BinaryTarget | undefined;
readonly route: SessionProviderRoute;
readonly router: SessionProviderRouter;
readonly runtimeAvailability: ReadonlyMap<
readonly runtimeUnavailability: ReadonlyMap<
string,
{ readonly available: boolean; readonly reason: string | null }
{ readonly reason: string }
>;
readonly runId: string | undefined;
readonly runtimeLineageSnapshots: readonly ProviderRuntimeLineageSnapshot[];
@@ -28,7 +28,7 @@ export const binarySessionStatus = ({
target,
route,
router,
runtimeAvailability,
runtimeUnavailability,
runId,
runtimeLineageSnapshots,
requestActivitySnapshots,
@@ -38,10 +38,16 @@ export const binarySessionStatus = ({
const providers = router.providerIdentities(route).map(providerSummary);
const capabilities = [...(route.capabilities?.values() ?? [])]
.sort((left, right) => left.operation.localeCompare(right.operation))
.map((descriptor) => ({
...descriptor,
...runtimeAvailability.get(descriptor.operation),
}))
.map((descriptor) => {
const unavailable = runtimeUnavailability.get(descriptor.operation);
return unavailable === undefined
? descriptor
: {
...descriptor,
available: false as const,
reason: unavailable.reason,
};
})
.map(capabilityStatus);
const analysisProviderBinding =
route.binding === null
+3 -7
View File
@@ -93,11 +93,7 @@ const browserParameters = (
input: BrowserEvidenceInput,
): EvidenceObservation["parameters"] => {
if (!("cdp_endpoint" in input)) {
if (
"before_scenario" in input &&
input.before_scenario !== undefined &&
input.after_scenario !== undefined
)
if ("before_scenario" in input)
return {
comparison_kind: "browser_scenario",
before_browser: input.before_scenario.browser,
@@ -166,9 +162,9 @@ const browserParameters = (
include_storage_keys: input.include_storage_keys,
include_storage_fingerprints: input.include_storage_fingerprints,
limits: input.limits,
...(input.include_script_sources && "source_capture_approved" in input
source_capture_approved: input.source_capture_approved,
...(input.include_script_sources && "analysis_limits" in input
? {
source_capture_approved: input.source_capture_approved,
fetch_source_maps: input.fetch_source_maps,
source_map_fetch_approved: input.source_map_fetch_approved,
analysis_limits: input.analysis_limits,
@@ -0,0 +1,47 @@
import type { ClientFeatureAvailability } from "../contracts/toolOutputSchemaPrimitives.js";
type ClientFeatureName = keyof ClientFeatureAvailability;
/** Empty MCP client feature declaration used when the caller supplies none. */
export const NO_CLIENT_FEATURES: ClientFeatureAvailability = {
elicitation_form: false,
elicitation_url: false,
roots: false,
sampling: false,
};
const CLIENT_FEATURE_REQUIREMENTS: Readonly<
Record<
string,
{
readonly required: readonly ClientFeatureName[];
readonly optional: readonly ClientFeatureName[];
}
>
> = {
capture_process_scenario: {
required: [],
optional: ["elicitation_form"],
},
};
/** Project a tool's client requirements against the negotiated feature set. */
export const clientRequirementsFor = (
name: string,
clientFeatures: ClientFeatureAvailability,
) => {
const requirements = CLIENT_FEATURE_REQUIREMENTS[name] ?? {
required: [],
optional: [],
};
return {
required: [...requirements.required],
optional: [...requirements.optional],
missing_required: requirements.required.filter(
(feature) => !clientFeatures[feature],
),
missing_optional: requirements.optional.filter(
(feature) => !clientFeatures[feature],
),
};
};
+202 -101
View File
@@ -1,39 +1,31 @@
import { z } from "zod";
import { TOOL_CONTRACTS } from "../contracts/toolContracts.js";
import type { ClientFeatureAvailability } from "../contracts/toolOutputSchemaPrimitives.js";
import {
providerCapability,
type ClientFeatureAvailability,
type ProviderCapability,
type ToolAvailability,
type ToolAvailabilityMode,
type ToolUnavailabilityReason,
} from "../contracts/toolOutputSchemaPrimitives.js";
import type { ToolKind } from "../contracts/toolContractTypes.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
import {
clientRequirementsFor,
NO_CLIENT_FEATURES,
} from "./CapabilityClientRequirements.js";
const statusSchema = z.object({
open: z.boolean(),
kind: z.enum(["executable", "database", "archive", "artifact"]).optional(),
format: z.string().optional(),
capabilities: z.array(
z.object({
operation: z.string(),
available: z.boolean(),
reason: z.string().nullable(),
availability_code: z.string().nullable().optional(),
}),
),
capabilities: z.array(providerCapability),
});
type ToolAvailabilityReason =
| "available"
| "client_capability_missing"
| "target_required"
| "provider_missing"
| "provider_unavailable"
| "target_unsupported"
| "unsupported_host"
| "policy_disabled";
type ToolAvailabilityReason = "available" | ToolUnavailabilityReason;
type ToolKind = (typeof TOOL_CONTRACTS)[number]["kind"];
type ProviderDescriptor = {
readonly available: boolean;
readonly reason: string | null;
readonly availability_code?: string | null;
};
type ProviderDescriptor = ProviderCapability;
export type AvailabilityPolicy = {
readonly processCaptureEnabled: boolean;
readonly evidenceFileRoots: number;
@@ -41,38 +33,31 @@ export type AvailabilityPolicy = {
readonly browserObservationEnabled?: boolean;
readonly browserScenarioEnabled?: boolean;
readonly electronObservationEnabled?: boolean;
readonly electronAutomationEnabled?: boolean;
readonly v8InspectorObservationEnabled?: boolean;
readonly javascriptReplayEnabled?: boolean;
readonly managedRuntimeEnabled?: boolean;
};
type ClientFeatureName = keyof ClientFeatureAvailability;
const NO_CLIENT_FEATURES: ClientFeatureAvailability = {
elicitation_form: false,
elicitation_url: false,
roots: false,
sampling: false,
};
const CLIENT_FEATURE_REQUIREMENTS: Readonly<
Record<
string,
{
readonly required: readonly ClientFeatureName[];
readonly optional: readonly ClientFeatureName[];
}
>
> = {
capture_process_scenario: {
required: [],
optional: ["elicitation_form"],
},
};
type Availability = {
readonly reason: ToolAvailabilityReason;
readonly remediation: string | null;
type AvailabilityFacts = {
readonly defaultModeAvailable?: boolean;
readonly modes?: readonly ToolAvailabilityMode[];
};
type Availability = AvailabilityFacts &
(
| { readonly reason: "available"; readonly remediation: null }
| {
readonly reason: ToolUnavailabilityReason;
readonly remediation: string | null;
}
);
type NavigationModeResult =
| (Extract<ToolAvailabilityMode, { available: true }> & {
readonly reason: "available";
})
| (Extract<ToolAvailabilityMode, { available: false }> & {
readonly reason: ToolUnavailabilityReason;
});
type AvailabilityContext = {
readonly name: string;
readonly kind: ToolKind;
@@ -102,6 +87,7 @@ const ENHANCED_REQUIREMENTS: Readonly<Record<string, readonly string[]>> = {
"list_strings",
],
analyze_function: ["analyze_function"],
inspect_native_api: ["analyze_function"],
trace_feature: [
"list_strings",
"list_procedures",
@@ -110,24 +96,32 @@ const ENHANCED_REQUIREMENTS: Readonly<Record<string, readonly string[]>> = {
],
};
const NAVIGATION_CONTEXT_MODES = [
{
name: "current_selection",
requirements: [
"current_document",
"current_address",
"resolve_containing_procedure",
],
},
{
name: "explicit_document",
requirements: ["current_address", "resolve_containing_procedure"],
},
] as const;
/** Build stable per-operation availability for discovery and tool visibility. */
export const buildCapabilityInventory = (
sessionStatus: JsonValue,
policy: AvailabilityPolicy,
clientFeatures: ClientFeatureAvailability = NO_CLIENT_FEATURES,
) => {
): readonly ToolAvailability[] => {
const status = statusSchema.parse(sessionStatus);
const descriptors = new Map<string, ProviderDescriptor>(
status.capabilities.map((descriptor) => [
descriptor.operation,
{
available: descriptor.available,
reason: descriptor.reason,
availability_code: descriptor.availability_code ?? null,
},
]),
status.capabilities.map((descriptor) => [descriptor.operation, descriptor]),
);
return TOOL_CONTRACTS.map((contract) => {
return GENERATED_MCP_TOOL_CATALOG.map((contract): ToolAvailability => {
const availability = availabilityFor({
name: contract.name,
kind: contract.kind,
@@ -141,16 +135,15 @@ export const buildCapabilityInventory = (
clientFeatures,
);
const clientBlocked = clientRequirements.missing_required.length > 0;
return {
const facts = {
name: contract.name,
surface: contract.kind,
available: availability.reason === "available" && !clientBlocked,
reason: clientBlocked
? ("client_capability_missing" as const)
: availability.reason,
remediation: clientBlocked
? `Use an MCP client that supports: ${clientRequirements.missing_required.join(", ")}.`
: availability.remediation,
...(availability.defaultModeAvailable === undefined
? {}
: { default_mode_available: availability.defaultModeAvailable }),
...(availability.modes === undefined
? {}
: { modes: [...availability.modes] }),
client_requirements: clientRequirements,
effects: { ...contract.effects },
annotations: {
@@ -160,30 +153,32 @@ export const buildCapabilityInventory = (
open_world: contract.annotations.openWorldHint ?? true,
},
};
if (clientBlocked)
return {
...facts,
available: false,
reason: "client_capability_missing",
remediation: `Use an MCP client that supports: ${clientRequirements.missing_required.join(", ")}.`,
};
return availability.reason === "available"
? {
...facts,
available: true,
reason: availability.reason,
remediation: null,
}
: {
...facts,
available: false,
reason: availability.reason,
remediation: availability.remediation,
};
}).sort((left, right) => left.name.localeCompare(right.name));
};
const clientRequirementsFor = (
name: string,
clientFeatures: ClientFeatureAvailability,
) => {
const requirements = CLIENT_FEATURE_REQUIREMENTS[name] ?? {
required: [],
optional: [],
};
return {
required: [...requirements.required],
optional: [...requirements.optional],
missing_required: requirements.required.filter(
(feature) => !clientFeatures[feature],
),
missing_optional: requirements.optional.filter(
(feature) => !clientFeatures[feature],
),
};
};
const availabilityFor = (context: AvailabilityContext): Availability => {
if (context.name === "get_navigation_context")
return navigationContextAvailability(context.descriptors);
const javascriptApplication = javascriptApplicationAvailability(context);
if (javascriptApplication !== null) return javascriptApplication;
const policyDecision = policyAvailability(context);
@@ -248,14 +243,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 }
@@ -268,6 +257,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,
@@ -355,16 +367,20 @@ const providerAvailability = ({
const composedAvailability = (
name: string,
descriptors: ReadonlyMap<string, ProviderDescriptor>,
): {
readonly reason: ToolAvailabilityReason;
readonly remediation: string | null;
} => {
): Availability => {
const requirements = ENHANCED_REQUIREMENTS[name];
if (requirements === undefined)
return {
reason: "provider_missing",
remediation: "No provider composition is declared for this operation.",
};
return composedAvailabilityFor(requirements, descriptors);
};
const composedAvailabilityFor = (
requirements: readonly string[],
descriptors: ReadonlyMap<string, ProviderDescriptor>,
): Availability => {
const missing = requirements.find((operation) => !descriptors.has(operation));
if (missing !== undefined)
return {
@@ -385,3 +401,88 @@ const composedAvailability = (
};
return { reason: "available", remediation: null };
};
const navigationContextAvailability = (
descriptors: ReadonlyMap<string, ProviderDescriptor>,
): Availability => {
const modeResults: NavigationModeResult[] = NAVIGATION_CONTEXT_MODES.map(
(mode) => {
const availability = composedAvailabilityFor(
mode.requirements,
descriptors,
);
if (availability.reason === "available")
return {
name: mode.name,
available: true,
reason: availability.reason,
missing_operations: [],
remediation: null,
};
return {
name: mode.name,
available: false,
reason: availability.reason,
missing_operations: mode.requirements.filter((operation) => {
const descriptor = descriptors.get(operation);
return descriptor === undefined || descriptor.available === false;
}),
remediation: availability.remediation,
};
},
);
const modes: ToolAvailabilityMode[] = modeResults.map((mode) =>
mode.available
? {
name: mode.name,
available: true,
missing_operations: [],
remediation: null,
}
: {
name: mode.name,
available: false,
missing_operations: [...mode.missing_operations],
remediation: mode.remediation,
},
);
const availableMode = modeResults.find((mode) => mode.available);
const failures = modeResults.flatMap((mode) =>
mode.available ? [] : [mode],
);
const failure = failures.sort(
(left, right) =>
navigationFailurePriority(left.reason) -
navigationFailurePriority(right.reason),
)[0];
const specificFailureRemediation =
failure?.reason === "provider_missing" ? undefined : failure?.remediation;
const facts = {
defaultModeAvailable: modes[0]?.available ?? false,
modes,
};
if (availableMode !== undefined)
return { ...facts, reason: "available", remediation: null };
return {
...facts,
reason: failure?.reason ?? "provider_missing",
remediation:
specificFailureRemediation ??
`Configure a provider for one of: ${modes
.flatMap((mode) => mode.missing_operations)
.join(", ")}.`,
};
};
const navigationFailurePriority = (reason: ToolAvailabilityReason): number => {
switch (reason) {
case "unsupported_host":
return 0;
case "provider_unavailable":
return 1;
case "provider_missing":
return 2;
default:
return 3;
}
};
+6 -1
View File
@@ -5,6 +5,7 @@ import { parse as parseToml } from "smol-toml";
import { z } from "zod";
import { PRODUCT_IDENTITY } from "../identity.js";
import { MCP_STARTUP_POLICY } from "../mcpStartupPolicy.js";
import { supportedClients } from "./SupportedClients.js";
export interface ClientRegistrationStatus {
@@ -80,7 +81,11 @@ const registrationAligned = (
currentCommandPath: string,
): boolean => {
const command = [registration.command, ...registration.args];
if (client === "codex" && registration.startup_timeout_sec !== 30)
if (
client === "codex" &&
registration.startup_timeout_sec !==
MCP_STARTUP_POLICY.codexStartupTimeoutSeconds
)
return false;
if (
command.length === 4 &&
+10 -6
View File
@@ -20,6 +20,7 @@ import {
} from "./ArtifactInventory.js";
import { scanAuthorizedArtifactInventory } from "./AuthorizedArtifactInventory.js";
import { ARTIFACT_GRAPH_PROVIDER } from "./InvestigationProviders.js";
import { resolveArtifactIntegrityPolicy } from "./ArtifactInventory/policy.js";
export const AUTOMATIC_RUN_LIMITATION =
"Automatic cross-version runs compare shipped artifact structure only; they do not execute either version.";
@@ -76,12 +77,15 @@ export const scanVersions = async (
if (signal?.aborted === true)
return err(new AnalysisCancelledError("find_changed_behavior"));
const limits = artifactLimits(input.options);
const integrity: ArtifactIntegrityPolicy = {
mode: input.integrity_policy,
approved: input.integrity_continue_approved,
enabled: integrityContinueEnabled,
maxMismatches: input.max_integrity_mismatches,
};
const integrity = resolveArtifactIntegrityPolicy(
input.integrity_policy === "fail"
? { mode: "fail" }
: {
mode: input.integrity_policy,
maxMismatches: input.max_integrity_mismatches,
},
integrityContinueEnabled,
);
return ok(
await scanVersionInventories({
leftPath: input.left_path,
+1 -1
View File
@@ -67,7 +67,7 @@ export interface CrossVersionInvestigationExecution {
/** Run or resume a deterministic cross-version artifact investigation. */
export const runCrossVersionInvestigation = async (
input: CrossVersionInvestigationInput,
input: unknown,
policy: EvidenceFilePolicy,
execution: CrossVersionInvestigationExecution,
): Promise<Result<CrossVersionInvestigationOutcome, AnalysisError>> => {
+15 -3
View File
@@ -52,6 +52,7 @@ type DirectAnalysisTool =
| "procedure_pseudo_code"
| "read_function_instructions"
| "analyze_function"
| "inspect_native_api"
| "search_strings"
| "search_procedures"
| "xrefs"
@@ -341,6 +342,7 @@ const executeAnalysisTool = async (input: {
if (
tool === "binary_overview" ||
tool === "analyze_function" ||
tool === "inspect_native_api" ||
tool === "trace_feature"
) {
const result = await new EnhancedTools(session).execute(
@@ -438,7 +440,7 @@ const replayProviderFor = (
| ManagedToolName
| DirectAnalysisTool,
) =>
tool === "binary_overview" || tool === "trace_feature"
isWorkflowEvidenceTool(tool)
? REA_WORKFLOW_PROVIDER
: session.providerIdentity(tool);
@@ -450,12 +452,22 @@ const analysisProfileForEvidence = (
| ManagedToolName
| DirectAnalysisTool,
): AnalysisProfileCommitment | undefined => {
if (tool !== "binary_overview" && tool !== "trace_feature")
return session.analysisProfile(tool);
if (!isWorkflowEvidenceTool(tool)) return session.analysisProfile(tool);
const upstream = session.analysisProfile();
return upstream === undefined ? undefined : workflowAnalysisProfile(upstream);
};
const isWorkflowEvidenceTool = (
tool:
| NativeToolName
| ArtifactToolName
| ManagedToolName
| DirectAnalysisTool,
): boolean =>
tool === "binary_overview" ||
tool === "inspect_native_api" ||
tool === "trace_feature";
const fileExists = async (path: string): Promise<boolean> => {
try {
await access(path);
+81
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;
};
@@ -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>>;
}
@@ -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;
};
+54 -23
View File
@@ -1,3 +1,5 @@
import type { z } from "zod";
import type {
AnalysisOperation,
AnalysisOperationPort,
@@ -12,6 +14,7 @@ import {
} from "../domain/errors.js";
import {
addressDistance,
functionDossierSchema,
parseDocuments,
parseFunctionDossier,
parseListCount,
@@ -25,7 +28,7 @@ import {
discoverObjcProtocols,
discoverSwiftClasses,
} from "../domain/symbolAnalysis.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import {
invalidEnhancedInput,
@@ -35,6 +38,7 @@ import {
import { readAllAddressed } from "./EnhancedToolPagination.js";
import { traceCallPath } from "./CallPathTracing.js";
import { traceLiteralFeature } from "./EnhancedLiteralTracing.js";
import { projectNativeApiInspection } from "./NativeApiInspection.js";
export type { ValidatedEnhancedCall } from "./EnhancedToolTypes.js";
/**
@@ -57,6 +61,8 @@ export class EnhancedTools {
name === "trace_call_path"
)
return this.#executeTracing(name, input, signal);
if (name === "analyze_function" || name === "inspect_native_api")
return this.#executeFunctionAnalysis(name, input, signal);
switch (name) {
case "swift_classes": {
const parsed = enhancedInputSchemas.swift_classes.safeParse(input);
@@ -98,12 +104,6 @@ export class EnhancedTools {
? this.executeValidated({ name, input: parsed.data }, signal)
: invalidEnhancedInput(name, parsed.error);
}
case "analyze_function": {
const parsed = enhancedInputSchemas.analyze_function.safeParse(input);
return parsed.success
? this.executeValidated({ name, input: parsed.data }, signal)
: invalidEnhancedInput(name, parsed.error);
}
}
}
@@ -131,6 +131,8 @@ export class EnhancedTools {
return this.#binaryOverview(call.input, signal);
case "analyze_function":
return this.#analyzeFunction(call.input, signal);
case "inspect_native_api":
return this.#inspectNativeApi(call.input, signal);
case "trace_feature":
return traceLiteralFeature(
(name, arguments_, operationSignal) =>
@@ -162,23 +164,34 @@ export class EnhancedTools {
input: unknown,
signal?: AbortSignal,
): EnhancedResult {
const parsed = enhancedInputSchemas[name].safeParse(input);
if (name === "trace_call_path") {
const parsed = enhancedInputSchemas.trace_call_path.safeParse(input);
if (!parsed.success) return invalidEnhancedInput(name, parsed.error);
return this.executeValidated({ name, input: parsed.data }, signal);
}
if (name === "trace_feature") {
const parsed = enhancedInputSchemas.trace_feature.safeParse(input);
if (!parsed.success) return invalidEnhancedInput(name, parsed.error);
return this.executeValidated({ name, input: parsed.data }, signal);
}
const parsed = enhancedInputSchemas.find_code_for_string.safeParse(input);
if (!parsed.success) return invalidEnhancedInput(name, parsed.error);
if (name === "trace_call_path")
return this.executeValidated(
{
name,
input: enhancedInputSchemas.trace_call_path.parse(parsed.data),
},
signal,
);
return this.executeValidated(
{
name,
input: enhancedInputSchemas[name].parse(parsed.data),
},
signal,
);
return this.executeValidated({ name, input: parsed.data }, signal);
}
#executeFunctionAnalysis(
name: "analyze_function" | "inspect_native_api",
input: unknown,
signal?: AbortSignal,
): EnhancedResult {
if (name === "analyze_function") {
const parsed = enhancedInputSchemas.analyze_function.safeParse(input);
if (!parsed.success) return invalidEnhancedInput(name, parsed.error);
return this.executeValidated({ name, input: parsed.data }, signal);
}
const parsed = enhancedInputSchemas.inspect_native_api.safeParse(input);
if (!parsed.success) return invalidEnhancedInput(name, parsed.error);
return this.executeValidated({ name, input: parsed.data }, signal);
}
async #analyzeFunction(
@@ -189,6 +202,24 @@ export class EnhancedTools {
return result.ok ? parseFunctionDossier(result.value) : result;
}
async #inspectNativeApi(
input: z.output<typeof enhancedInputSchemas.inspect_native_api>,
signal?: AbortSignal,
): EnhancedResult {
const analysisInput = enhancedInputSchemas.analyze_function.parse({
procedure: input.procedure,
include_assembly: true,
max_pseudocode_chars: input.max_pseudocode_chars,
max_instructions: input.max_instructions,
});
const result = await this.#call("analyze_function", analysisInput, signal);
if (!result.ok) return result;
const parsed = parseFunctionDossier(result.value);
if (!parsed.ok) return parsed;
const dossier = functionDossierSchema.parse(parsed.value);
return ok(jsonValueSchema.parse(projectNativeApiInspection(dossier)));
}
async #swiftClasses(pattern: string, signal?: AbortSignal): EnhancedResult {
const procedures = await this.#allAddressed("list_procedures", signal);
return procedures.ok
+5 -1
View File
@@ -26,7 +26,11 @@ export const importEvidenceBundleCommand = async (
const ledger = createLedger();
const imported = ledger.import(loaded.value);
return imported.ok
? ok({ imported: imported.value, total: ledger.export().records.length })
? ok({
imported: imported.value.recordsAdded,
unknowns_added: imported.value.unknownsAdded,
total: ledger.export().records.length,
})
: imported;
};
+77 -20
View File
@@ -3,6 +3,7 @@ import canonicalize from "canonicalize";
import {
createEvidenceBundle,
parseEvidenceBundle,
validateResidualUnknownAddition,
type EvidenceBundle,
} from "../domain/evidenceBundle.js";
import {
@@ -28,7 +29,15 @@ export interface EvidenceLedgerLimits {
type EvidenceLedgerFailure = EvidenceIntegrityError | EvidenceLimitError;
type RecordResult = Result<"added" | "duplicate", EvidenceLedgerFailure>;
type ImportResult = Result<number, EvidenceLedgerFailure>;
/** Describes the state changes made by one atomic bundle import. */
export interface EvidenceImportDelta {
readonly recordsAdded: number;
readonly unknownsAdded: number;
readonly changed: boolean;
}
type ImportResult = Result<EvidenceImportDelta, EvidenceLedgerFailure>;
/** Bounded, session-owned set of immutable evidence records. */
export class EvidenceLedger {
@@ -97,6 +106,7 @@ export class EvidenceLedger {
pending.set(evidence.evidence_id, evidence);
}
const pendingUnknowns = new Map(this.#unknownRevisions);
let unknownsAdded = 0;
for (const unknown of bundle.unknowns) {
const key = unknownRevisionKey(unknown);
const existing = pendingUnknowns.get(key);
@@ -106,6 +116,7 @@ export class EvidenceLedger {
)
return err(new EvidenceIntegrityError("Conflicting unknown revision"));
pendingUnknowns.set(key, unknown);
if (existing === undefined) unknownsAdded += 1;
}
const checked = this.#validateCandidate(pending, pendingUnknowns);
if (!checked.ok) {
@@ -122,7 +133,11 @@ export class EvidenceLedger {
}
const added = pending.size - this.#records.size;
this.#commit(pending, pendingUnknowns, checked.value);
return ok(added);
return ok({
recordsAdded: added,
unknownsAdded,
changed: added > 0 || unknownsAdded > 0,
});
}
/** Export records in deterministic, semantically irrelevant ID order. */
@@ -216,8 +231,6 @@ export class EvidenceLedger {
} catch (cause: unknown) {
return err(new UnknownRegistryError("invalid-transition", { cause }));
}
const pendingRecords = new Map(this.#records);
pendingRecords.set(evidence.evidence_id, evidence);
const existingUnknown = this.#unknownHeads.get(unknown.unknown_id);
if (existingUnknown !== undefined) {
if (
@@ -225,20 +238,12 @@ export class EvidenceLedger {
existingUnknown.revision_digest !== unknown.revision_digest
)
return err(new UnknownRegistryError("already-exists"));
const checked = this.#validateCandidate(
pendingRecords,
this.#unknownRevisions,
);
const checked = this.#appendIncremental([evidence]);
if (!checked.ok) return checked;
this.#commit(pendingRecords, this.#unknownRevisions, checked.value);
return ok(null);
}
pendingRecords.set(mutation.evidence_id, mutation);
const pendingUnknowns = new Map(this.#unknownRevisions);
pendingUnknowns.set(unknownRevisionKey(unknown), unknown);
const checked = this.#validateCandidate(pendingRecords, pendingUnknowns);
const checked = this.#appendIncremental([evidence, mutation], unknown);
if (!checked.ok) return checked;
this.#commit(pendingRecords, pendingUnknowns, checked.value);
return ok(structuredClone(unknown));
}
@@ -332,16 +337,68 @@ export class EvidenceLedger {
}),
);
}
const pendingRecords = new Map(this.#records);
pendingRecords.set(mutationEvidence.evidence_id, mutationEvidence);
const pendingUnknowns = new Map(this.#unknownRevisions);
pendingUnknowns.set(unknownRevisionKey(unknown), unknown);
const checked = this.#validateCandidate(pendingRecords, pendingUnknowns);
const checked = this.#appendIncremental([mutationEvidence], unknown);
if (!checked.ok) return checked;
this.#commit(pendingRecords, pendingUnknowns, checked.value);
return ok(structuredClone(unknown));
}
#appendIncremental(
records: readonly Evidence[],
unknown?: ResidualUnknown,
): Result<
void,
EvidenceIntegrityError | EvidenceLimitError | UnknownRegistryError
> {
const additions = new Map<string, Evidence>();
let addedBytes = 0;
for (const evidence of records) {
const existing =
additions.get(evidence.evidence_id) ??
this.#records.get(evidence.evidence_id);
if (existing !== undefined) {
if (!recordsAgree(existing, evidence))
return err(new EvidenceIntegrityError("Conflicting evidence record"));
continue;
}
additions.set(evidence.evidence_id, evidence);
addedBytes += serializedBytes(evidence);
}
const unknownKey =
unknown === undefined ? undefined : unknownRevisionKey(unknown);
if (unknownKey !== undefined && this.#unknownRevisions.has(unknownKey))
return err(new UnknownRegistryError("integrity"));
const unknownBytes = unknown === undefined ? 0 : serializedBytes(unknown);
if (
this.#exceedsRecordLimit(
this.#records.size + additions.size,
this.#unknownRevisions.size + (unknown === undefined ? 0 : 1),
)
)
return err(new EvidenceLimitError("records", this.limits.maxRecords));
if (this.#bytes + addedBytes + unknownBytes > this.limits.maxBytes)
return err(new EvidenceLimitError("bytes", this.limits.maxBytes));
if (unknown !== undefined)
try {
validateResidualUnknownAddition(
unknown,
{
get: (id) => additions.get(id) ?? this.#records.get(id),
has: (id) => additions.has(id) || this.#records.has(id),
},
this.#unknownHeads,
);
} catch (cause: unknown) {
return err(new UnknownRegistryError("integrity", { cause }));
}
for (const [id, evidence] of additions) this.#records.set(id, evidence);
if (unknown !== undefined && unknownKey !== undefined) {
this.#unknownRevisions.set(unknownKey, unknown);
this.#unknownHeads.set(unknown.unknown_id, unknown);
}
this.#bytes += addedBytes + unknownBytes;
return ok(undefined);
}
#validateCandidate(
records: ReadonlyMap<string, Evidence>,
unknowns: ReadonlyMap<string, ResidualUnknown>,
@@ -11,7 +11,7 @@ import {
type EvidenceProvider,
type EvidenceSubjectTarget,
} from "../domain/evidence.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js";
interface InventoryProjectionInput {
@@ -71,7 +71,7 @@ export const projectInventoryEvidence = <
inventory_evidence_ids: [...result.source_evidence_ids],
limits: jsonValueSchema.parse(parsed.data.limits),
},
result: jsonValueSchema.parse(result) as JsonValue,
result: jsonValueSchema.parse(result),
rawResult: null,
confidence: "inferred",
authority: "analyst-inference",
+23 -10
View File
@@ -1,16 +1,18 @@
import { createHash } from "node:crypto";
import { analyzeJavaScriptStaticSource } from "../domain/javascriptStaticAnalysis.js";
import { analyzeJavaScriptSemantics } from "../domain/javascriptSemanticAnalysis.js";
import { analyzeParsedJavaScriptStaticSource } from "../domain/javascriptStaticAnalysis.js";
import { analyzeParsedJavaScriptSemantics } from "../domain/javascriptSemanticAnalysis.js";
import {
DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS,
type JavaScriptSemanticLimits,
} from "../domain/javascriptSemanticIr.js";
import { parseJavaScriptSource } from "../domain/javascriptSourceParser.js";
import type {
JavaScriptSourceRange,
JavaScriptSourcePoint,
JavaScriptStaticAnalysis,
} from "../domain/javascriptStaticAnalysisTypes.js";
import { failedJavaScriptStaticAnalysis } from "../domain/javascriptStaticAnalysisHelpers.js";
import type {
JavaScriptArtifactFile,
JavaScriptArtifactFileSet,
@@ -142,13 +144,24 @@ const analyzeArtifactFile = (
state.truncatedScopes += 1;
return;
}
const analysis = analyzeJavaScriptStaticSource(file.text.value, {
maxAstNodes: remainingNodes,
maxFindings: remainingFindings,
maxModules: remainingModules,
deadline: context.deadline,
now: context.now,
});
const parsed = parseJavaScriptSource(file.text.value);
if (parsed === null) {
const analysis = failedJavaScriptStaticAnalysis();
state.files.push({ file, javascript: analysis, semantic: null });
state.parseFailures += 1;
return;
}
const analysis = analyzeParsedJavaScriptStaticSource(
file.text.value,
parsed,
{
maxAstNodes: remainingNodes,
maxFindings: remainingFindings,
maxModules: remainingModules,
deadline: context.deadline,
now: context.now,
},
);
const staticFindings = findingCount(analysis);
const semanticFindingBudget = Math.max(
0,
@@ -164,7 +177,7 @@ const analyzeArtifactFile = (
};
const semantics =
analysis.parse_status === "complete" || analysis.parse_status === "partial"
? analyzeJavaScriptSemantics(file.text.value, semanticLimits)
? analyzeParsedJavaScriptSemantics(parsed, semanticLimits)
: null;
state.files.push({
file,
@@ -1,12 +1,10 @@
import type { z } from "zod";
import type { controlledReplayInputSchema } from "../domain/javascriptReplay.js";
import type { JavaScriptReplayPolicy } from "./JavaScriptReplayPlanning.js";
import type { ControlledReplayInput } from "../domain/javascriptReplay.js";
import type { EnabledJavaScriptReplayPolicy } from "./JavaScriptReplayPlanning.js";
/** Build the exact permission request for one controlled replay operation. */
export const replayPermissionRequest = (
input: z.output<typeof controlledReplayInputSchema>,
policy: JavaScriptReplayPolicy,
input: ControlledReplayInput,
policy: EnabledJavaScriptReplayPolicy,
) => ({
capability: "javascript_replay" as const,
roots: [...input.left.modules, ...(input.right?.modules ?? [])].map(
@@ -22,5 +20,5 @@ export const replayPermissionRequest = (
environment_names: [],
network: "none" as const,
mount: true,
operation_identity: `run_controlled_replay:${input.plan_digest ?? "plan"}`,
operation_identity: `run_controlled_replay:${input.mode === "plan" ? "plan" : input.plan_digest}`,
});
+19 -13
View File
@@ -37,9 +37,10 @@ export interface ReplayRuntimeFileIdentity {
readonly sha256: string;
}
export interface JavaScriptReplayPolicy {
readonly enabled: boolean;
readonly roots: readonly string[];
/** Complete executable and source authority for enabled JavaScript replay. */
export interface EnabledJavaScriptReplayPolicy {
readonly status: "enabled";
readonly roots: readonly [string, ...string[]];
readonly nodePath: string;
readonly bubblewrapPath: string;
readonly systemdRunPath: string;
@@ -47,6 +48,11 @@ export interface JavaScriptReplayPolicy {
readonly shellPath: string;
}
/** Parsed JavaScript replay configuration. */
export type JavaScriptReplayPolicy =
| { readonly status: "disabled" }
| EnabledJavaScriptReplayPolicy;
export interface JavaScriptReplayHost {
readonly readSource: (
path: string,
@@ -61,7 +67,7 @@ export interface JavaScriptReplayHost {
nodePath: string,
) => Promise<readonly ReplayRuntimeFileIdentity[]>;
readonly seccompDigest: () => string;
readonly probe: (policy: JavaScriptReplayPolicy) => Promise<void>;
readonly probe: (policy: EnabledJavaScriptReplayPolicy) => Promise<void>;
}
export interface PreparedReplayPlan {
@@ -73,7 +79,7 @@ export interface PreparedReplayPlan {
export interface JavaScriptReplayRunner {
readonly execute: (
prepared: PreparedReplayPlan,
policy: JavaScriptReplayPolicy,
policy: EnabledJavaScriptReplayPolicy,
signal?: AbortSignal,
) => Promise<ReplayExecutionResult>;
}
@@ -81,7 +87,7 @@ export interface JavaScriptReplayRunner {
/** Read, normalize, and content-address every input to one replay experiment. */
export const prepareReplayPlan = async (
input: ControlledReplayInput,
policy: JavaScriptReplayPolicy,
policy: EnabledJavaScriptReplayPolicy,
host: JavaScriptReplayHost,
): Promise<PreparedReplayPlan> => {
const cases = replayCases(input).map((item) => ({
@@ -243,13 +249,13 @@ const replayCases = (
return state >>> 0;
};
const corpus = boundaryCorpus(input.generator.preset);
const generated = Array.from(
{ length: input.generator.count },
(_, index) => ({
case_id: `generated-${input.generator?.preset ?? "boundary"}-${String(index)}`,
arguments: [corpus[next() % corpus.length] ?? ""] as JsonValue[],
}),
);
const generated: {
readonly case_id: string;
readonly arguments: JsonValue[];
}[] = Array.from({ length: input.generator.count }, (_, index) => ({
case_id: `generated-${input.generator?.preset ?? "boundary"}-${String(index)}`,
arguments: [corpus[next() % corpus.length] ?? ""],
}));
return uniqueCases([...explicit, ...generated]);
};
+45 -35
View File
@@ -6,6 +6,8 @@ import {
replayExecutionResultSchema,
controlledReplayOutputSchema,
replayEvidenceSchema,
type ControlledReplayExecutionInput,
type ControlledReplayInput,
} from "../domain/javascriptReplay.js";
import {
AnalysisCapabilityUnavailableError,
@@ -26,6 +28,7 @@ import { replayPermissionRequest } from "./JavaScriptReplayPermission.js";
import {
digestBytes,
prepareReplayPlan,
type EnabledJavaScriptReplayPolicy,
type JavaScriptReplayHost,
type JavaScriptReplayPolicy,
type JavaScriptReplayRunner,
@@ -36,7 +39,7 @@ const isAborted = (signal: AbortSignal | undefined): boolean =>
signal?.aborted === true;
export interface JavaScriptReplayDependencies {
readonly policy: JavaScriptReplayPolicy;
readonly policy: () => JavaScriptReplayPolicy;
readonly host: JavaScriptReplayHost;
readonly runner: JavaScriptReplayRunner;
readonly authority: PermissionAuthority | undefined;
@@ -63,14 +66,20 @@ export const runControlledReplay = async (
/** Plan or execute replay input already parsed by a trusted adapter. */
export const runControlledReplayValidated = async (
dependencies: JavaScriptReplayDependencies,
input: z.output<typeof controlledReplayInputSchema>,
input: ControlledReplayInput,
options: ExecutionOptions = {},
): Promise<Result<JsonValue, AnalysisError>> => {
const authorized = await authorizeReplay(dependencies, input, options.signal);
const authorized = await authorizeReplay(
dependencies,
dependencies.policy(),
input,
options.signal,
);
if (!authorized.ok) return authorized;
const prepared = await prepareValidatedReplay(
dependencies,
authorized.value,
input,
options.signal,
);
@@ -91,7 +100,7 @@ export const runControlledReplayValidated = async (
if (input.plan_digest !== prepared.value.publicPlan.plan_digest)
return err(
new ReplayPlanStaleError(
input.plan_digest ?? "",
input.plan_digest,
prepared.value.publicPlan.plan_digest,
),
);
@@ -103,12 +112,13 @@ export const runControlledReplayValidated = async (
);
if (!exportContext.ok) return exportContext;
const executed = await executeValidatedReplay(
dependencies,
prepared.value,
exportContext.value,
const executed = await executeValidatedReplay({
runner: dependencies.runner,
policy: authorized.value,
prepared: prepared.value,
exportContext: exportContext.value,
options,
);
});
if (!executed.ok) return executed;
try {
@@ -133,10 +143,11 @@ export const runControlledReplayValidated = async (
const authorizeReplay = async (
dependencies: JavaScriptReplayDependencies,
input: z.output<typeof controlledReplayInputSchema>,
policy: JavaScriptReplayPolicy,
input: ControlledReplayInput,
signal: AbortSignal | undefined,
): Promise<Result<true, AnalysisError>> => {
if (!dependencies.policy.enabled)
): Promise<Result<EnabledJavaScriptReplayPolicy, AnalysisError>> => {
if (policy.status === "disabled")
return err(
new AnalysisCapabilityUnavailableError(
"rea-javascript-replay",
@@ -154,7 +165,7 @@ const authorizeReplay = async (
);
if (isAborted(signal)) return err(new AnalysisCancelledError(OPERATION));
const request = replayPermissionRequest(input, dependencies.policy);
const request = replayPermissionRequest(input, policy);
const authorized =
input.mode === "plan"
? await dependencies.authority.explain(request, "read")
@@ -167,21 +178,20 @@ const authorizeReplay = async (
cause: authorized.error,
}),
);
return ok(true);
return ok(policy);
};
const prepareValidatedReplay = async (
dependencies: JavaScriptReplayDependencies,
input: z.output<typeof controlledReplayInputSchema>,
policy: EnabledJavaScriptReplayPolicy,
input: ControlledReplayInput,
signal: AbortSignal | undefined,
): Promise<
Result<Awaited<ReturnType<typeof prepareReplayPlan>>, AnalysisError>
> => {
if (isAborted(signal)) return err(new AnalysisCancelledError(OPERATION));
try {
return ok(
await prepareReplayPlan(input, dependencies.policy, dependencies.host),
);
return ok(await prepareReplayPlan(input, policy, dependencies.host));
} catch (cause: unknown) {
return err(
new AnalysisCapabilityUnavailableError(
@@ -200,12 +210,10 @@ interface ReproducerExportContext {
const authorizeReproducerExport = async (
authority: PermissionAuthority | undefined,
input: z.output<typeof controlledReplayInputSchema>,
input: ControlledReplayExecutionInput,
prepared: Awaited<ReturnType<typeof prepareReplayPlan>>,
): Promise<Result<ReproducerExportContext | undefined, AnalysisError>> => {
if (input.reproducer_export === undefined) return ok(undefined);
if (input.reproducer_export.approved !== true)
return err(new AnalysisInputError(`${OPERATION}:reproducer_export`));
if (authority === undefined)
return err(
new AnalysisCapabilityUnavailableError(
@@ -249,11 +257,16 @@ const authorizeReproducerExport = async (
});
};
interface ReplayExecutionContext {
readonly runner: JavaScriptReplayRunner;
readonly policy: EnabledJavaScriptReplayPolicy;
readonly prepared: Awaited<ReturnType<typeof prepareReplayPlan>>;
readonly exportContext: ReproducerExportContext | undefined;
readonly options: ExecutionOptions;
}
const executeValidatedReplay = async (
dependencies: JavaScriptReplayDependencies,
prepared: Awaited<ReturnType<typeof prepareReplayPlan>>,
exportContext: ReproducerExportContext | undefined,
options: ExecutionOptions,
context: ReplayExecutionContext,
): Promise<
Result<
{
@@ -263,6 +276,7 @@ const executeValidatedReplay = async (
AnalysisError
>
> => {
const { runner, policy, prepared, exportContext, options } = context;
await options.progress?.report({
phase: OPERATION,
completed: 0,
@@ -274,11 +288,7 @@ const executeValidatedReplay = async (
try {
let executed = replayExecutionResultSchema.parse(
await dependencies.runner.execute(
prepared,
dependencies.policy,
options.signal,
),
await runner.execute(prepared, policy, options.signal),
);
await options.progress?.report({
phase: OPERATION,
@@ -505,9 +515,9 @@ const createReplayEvidence = (
);
};
const asJson = (value: unknown): JsonValue =>
jsonValueSchema.parse(
JSON.parse(
JSON.stringify(controlledReplayOutputSchema.parse(value)),
) as unknown,
const asJson = (value: unknown): JsonValue => {
const serialized: unknown = JSON.parse(
JSON.stringify(controlledReplayOutputSchema.parse(value)),
);
return jsonValueSchema.parse(serialized);
};
+113
View File
@@ -0,0 +1,113 @@
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
import type { BinaryTarget } from "../domain/binaryTarget.js";
import type {
AnalysisClient,
AnalysisClientContext,
AnalysisProvider,
CapabilityDescriptor,
ProviderIdentity,
ProviderRequestActivitySnapshot,
ProviderRuntimeLineageSnapshot,
} from "./AnalysisProvider.js";
type LoadAnalysisProvider = () => Promise<AnalysisProvider>;
/**
* Preserve synchronous provider discovery while loading implementation modules
* only when a target-bound client first executes.
*/
export class LazyAnalysisProvider implements AnalysisProvider {
readonly #providerIdentity: ProviderIdentity;
readonly #capabilityDescriptors: readonly CapabilityDescriptor[];
readonly #loadProvider: LoadAnalysisProvider;
#provider: Promise<AnalysisProvider> | undefined;
constructor(options: {
readonly identity: ProviderIdentity;
readonly capabilities: readonly CapabilityDescriptor[];
readonly load: LoadAnalysisProvider;
}) {
this.#providerIdentity = options.identity;
this.#capabilityDescriptors = options.capabilities;
this.#loadProvider = options.load;
}
/** Return generated provider identity without loading its implementation. */
identity(): ProviderIdentity {
return this.#providerIdentity;
}
/** Return generated capability metadata without loading its implementation. */
capabilities(): readonly CapabilityDescriptor[] {
return this.#capabilityDescriptors;
}
/** Create a client whose implementation is loaded on first execution. */
createClient(
target: BinaryTarget,
profile?: AnalysisProfileCommitment,
context?: AnalysisClientContext,
): AnalysisClient {
return new LazyAnalysisClient(async () => {
const provider = await this.#load();
return provider.createClient(target, profile, context);
});
}
async #load(): Promise<AnalysisProvider> {
this.#provider ??= this.#loadProvider().catch((cause: unknown) => {
this.#provider = undefined;
throw cause;
});
return this.#provider;
}
}
class LazyAnalysisClient implements AnalysisClient {
readonly #loadClient: () => Promise<AnalysisClient>;
#client: AnalysisClient | undefined;
#loading: Promise<AnalysisClient> | undefined;
#closed = false;
constructor(loadClient: () => Promise<AnalysisClient>) {
this.#loadClient = loadClient;
}
execute: AnalysisClient["execute"] = async (
operation,
parameters,
options,
) => {
const client = await this.#load();
return client.execute(operation, parameters, options);
};
runtimeLineageSnapshots(): readonly ProviderRuntimeLineageSnapshot[] {
return this.#client?.runtimeLineageSnapshots?.() ?? [];
}
requestActivitySnapshots(): readonly ProviderRequestActivitySnapshot[] {
return this.#client?.requestActivitySnapshots?.() ?? [];
}
async close(): Promise<void> {
this.#closed = true;
if (this.#loading === undefined) return;
const client = await this.#loading;
await client.close();
}
async #load(): Promise<AnalysisClient> {
if (this.#closed) throw new Error("Lazy analysis client is closed");
this.#loading ??= this.#loadClient()
.then((client) => {
this.#client = client;
return client;
})
.catch((cause: unknown) => {
this.#loading = undefined;
throw cause;
});
return this.#loading;
}
}
+1 -1
View File
@@ -500,12 +500,12 @@ export const startLoopbackReplay = async (
async close() {
closePromise ??= (async () => {
recorder.stopMachineAdmission();
await recorder.drainMachine();
for (const client of websocket.clients) client.terminate();
await new Promise<void>((resolveClose) =>
websocket.close(() => resolveClose()),
);
await closeReplayServer(server);
await recorder.drainMachine();
})();
await closePromise;
},
@@ -23,14 +23,23 @@ import { digestJson } from "./JavaScriptReplayPlanning.js";
const OPERATION = "plan_managed_runtime_correlation" as const;
export interface ManagedRuntimePolicy {
readonly enabled: boolean;
readonly roots: readonly string[];
readonly executablePath: string;
}
/** Parsed availability and authority scope for managed runtime correlation. */
export type ManagedRuntimePolicy =
| { readonly status: "disabled" }
| {
readonly status: "enabled";
readonly roots: readonly [string, ...string[]];
readonly executablePath: string;
};
/** A managed-runtime policy that grants no runtime-correlation authority. */
export const MANAGED_RUNTIME_DISABLED = {
status: "disabled",
} as const satisfies ManagedRuntimePolicy;
/** Live policy and authority dependencies for managed runtime correlation. */
export interface ManagedRuntimeCorrelationDependencies {
readonly policy: ManagedRuntimePolicy;
readonly policy: () => ManagedRuntimePolicy;
readonly authority: PermissionAuthority | undefined;
}
@@ -53,7 +62,8 @@ export const planManagedRuntimeCorrelationEvidenceValidated = async (
dependencies: ManagedRuntimeCorrelationDependencies,
input: ManagedRuntimeCorrelationInput,
): Promise<Result<Evidence, AnalysisError>> => {
if (!dependencies.policy.enabled)
const policy = dependencies.policy();
if (policy.status === "disabled")
return err(
new AnalysisCapabilityUnavailableError(
MANAGED_WORKFLOW_PROVIDER.id,
@@ -69,8 +79,7 @@ export const planManagedRuntimeCorrelationEvidenceValidated = async (
"managed runtime permission policy is not configured",
),
);
let staticEvidence: Evidence;
let artifactPath: string;
let staticEvidence: Evidence, artifactPath: string;
try {
staticEvidence = parseEvidence(input.static_members);
artifactPath = artifactPathFor(staticEvidence);
@@ -81,7 +90,7 @@ export const planManagedRuntimeCorrelationEvidenceValidated = async (
{
capability: "managed_runtime",
roots: [artifactPath],
executables: [dependencies.policy.executablePath],
executables: [policy.executablePath],
environment_names: [],
network: "none",
mount: false,
@@ -101,7 +110,7 @@ export const planManagedRuntimeCorrelationEvidenceValidated = async (
try {
const result = planManagedRuntimeCorrelation(
input,
dependencies.policy.executablePath,
policy.executablePath,
authorized.value.grant_id,
);
return ok(
+113
View File
@@ -0,0 +1,113 @@
import type { FunctionDossier } from "../domain/hopperValues.js";
import {
nativeApiInspectionResultSchema,
type NativeApiBoundary,
type NativeApiInspectionResult,
} from "../domain/nativeApiBoundary.js";
const unavailableBoundary = (): NativeApiBoundary => ({
available: false,
reason:
"The selected provider did not expose a structured native API boundary model.",
residual_unknowns: [
"What are the function's return and parameter boundary types?",
"Does this function dispatch through a jump table with recoverable data addresses?",
],
});
/** Project one analyzed-function dossier into inspectable native API substeps. */
export const projectNativeApiInspection = (
dossier: FunctionDossier,
): NativeApiInspectionResult => {
const boundary = dossier.native_api ?? unavailableBoundary();
const unsupportedBranches = boundary.available
? []
: ["structured-boundary-types", "jump-table-data-mapping"];
const residualUnknowns = boundary.available
? availableResidualUnknowns(boundary)
: boundary.residual_unknowns;
return nativeApiInspectionResultSchema.parse({
schema_version: 1,
procedure: {
address: dossier.procedure.address,
name: dossier.procedure.name,
},
boundary,
substeps: [
{
operation: "analyze_function",
status: "completed",
observations: [
"provider function dossier",
"provider pseudocode classification",
"provider control-flow observations",
],
},
{
operation: "project_native_api_boundary",
status: boundary.available ? "completed" : "unsupported",
observations: boundary.available
? [
"return and parameter boundary types",
"confidence and inference evidence",
"jump-table dispatch, data, and target addresses",
]
: [boundary.reason],
},
{
operation: "preserve_residual_unknowns",
status: "completed",
observations:
residualUnknowns.length === 0
? ["no additional structured-boundary unknowns"]
: residualUnknowns,
},
],
unsupported_branches: unsupportedBranches,
residual_unknowns: residualUnknowns,
});
};
const availableResidualUnknowns = (
boundary: Extract<NativeApiBoundary, { readonly available: true }>,
): readonly string[] => {
const unknowns: string[] = [];
if (boundary.parameters_truncated)
unknowns.push(
"Which additional parameters were omitted by the native API boundary limit?",
);
if (boundary.jump_tables_truncated)
unknowns.push(
"Which additional jump tables were omitted by the native API boundary limit?",
);
for (const table of boundary.jump_tables) {
if (table.data_sources_truncated || table.mappings_truncated)
unknowns.push(
`Which additional data sources or targets were omitted for the jump table dispatched at ${table.dispatch_address}?`,
);
if (table.data_sources.length === 0)
unknowns.push(
`What backing data address produced the jump table dispatched at ${table.dispatch_address}?`,
);
if (
table.data_sources.some(
({ entry_count: count, entry_size_bytes: size }) =>
count === null || size === null,
)
)
unknowns.push(
`What are the exact entry size and count for candidate jump-table data dispatched at ${table.dispatch_address}?`,
);
if (table.mappings.some(({ case_value: value }) => value === null))
unknowns.push(
`Which source-level case values correspond to every target dispatched at ${table.dispatch_address}?`,
);
}
for (const value of [boundary.return_type, ...boundary.parameters]) {
if (value.confidence === "low")
unknowns.push(
`Can the inferred ${value.role} type ${value.data_type} be confirmed with imported metadata or an ABI probe?`,
);
}
return [...new Set(unknowns)];
};
@@ -16,7 +16,10 @@ import {
type AnalysisError,
} from "../domain/errors.js";
import { createEvidence, type Evidence } from "../domain/evidence.js";
import { controlledReplayOutputSchema } from "../domain/javascriptReplay.js";
import {
controlledReplayOutputSchema,
type ControlledReplayExecutionOutput,
} from "../domain/javascriptReplay.js";
import type { ReplayPlan } from "../domain/javascriptReplay.js";
import type { JavaScriptExportInstrumentation } from "../domain/javascriptExportInstrumentation.js";
import {
@@ -89,7 +92,7 @@ export const executeNodeCharacterization = async (
const plannedReplay = controlledReplayOutputSchema.parse(
prepared.value.replay,
);
if (plannedReplay.phase !== "plan" || plannedReplay.plan === null)
if (plannedReplay.phase !== "plan")
return err(
new AnalysisProtocolError(
"Node characterization preparation did not produce a replay plan",
@@ -110,6 +113,13 @@ export const executeNodeCharacterization = async (
options,
);
if (!executed.ok) return executed;
const replayOutput = controlledReplayOutputSchema.parse(executed.value);
if (replayOutput.phase !== "execute")
return err(
new AnalysisProtocolError(
"Node characterization execution omitted replay Evidence",
),
);
if (host.instrumentation === null)
return err(
new AnalysisProtocolError(
@@ -124,11 +134,8 @@ export const executeNodeCharacterization = async (
plan: prepared.value.plan,
transformation: host.instrumentation.manifest,
transformation_evidence: prepared.value.transformation_evidence,
evidence: createCharacterizationEvidence(
prepared.value,
controlledReplayOutputSchema.parse(executed.value),
),
replay: controlledReplayOutputSchema.parse(executed.value),
evidence: createCharacterizationEvidence(prepared.value, replayOutput),
replay: replayOutput,
}),
),
);
@@ -164,11 +171,7 @@ const prepareValidated = async (
);
if (!replay.ok) return replay;
const output = controlledReplayOutputSchema.parse(replay.value);
if (
output.phase !== "plan" ||
output.plan === null ||
host.instrumentation === null
)
if (output.phase !== "plan" || host.instrumentation === null)
return err(
new AnalysisProtocolError(
"Node characterization preparation omitted committed plan evidence",
@@ -240,12 +243,8 @@ const createCharacterizationEvidence = (
readonly plan: RuntimeCharacterizationPlan;
readonly transformation_evidence: Evidence;
},
replay: z.output<typeof controlledReplayOutputSchema>,
replay: ControlledReplayExecutionOutput,
): Evidence => {
if (replay.phase !== "execute" || replay.evidence === null)
throw new AnalysisProtocolError(
"Node characterization execution omitted replay Evidence",
);
return createEvidence(
{
path: prepared.plan.artifact.path,
+2 -2
View File
@@ -1,7 +1,7 @@
import { realpath } from "node:fs/promises";
import type {
ProcessExecutionPolicy,
EnabledProcessExecutionPolicy,
ProcessScenario,
} from "../domain/processCapture.js";
import {
@@ -16,7 +16,7 @@ const isWithin = (candidate: string, root: string): boolean =>
export const assertRealPathAuthority = async (
scenario: ProcessScenario,
policy: ProcessExecutionPolicy,
policy: EnabledProcessExecutionPolicy,
): Promise<void> => {
const executable = await realpath(scenario.executable);
const executableRoots = await canonicalizeConfiguredRoots(
@@ -420,6 +420,11 @@ export const prepareProcessCapture = async (
readonly home: string;
readonly before: SnapshotResult;
}> => {
if (policy.status === "disabled")
throw new ProcessCaptureError("process capture is disabled", {
userCategory: "permission_required",
userMessage: processPolicyMessage("process capture is disabled"),
});
const decision = authorizeProcessScenario(scenario, policy);
if (!decision.allowed)
throw new ProcessCaptureError(decision.reason, {

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