Compare commits

...
Author SHA1 Message Date
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
134 changed files with 8841 additions and 600 deletions
+56 -4
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 }}
@@ -25,11 +27,61 @@ jobs:
with:
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:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_PR_BRANCH: ${{ fromJSON(steps.release.outputs.pr).headBranchName }}
run: |
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
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "2.5.0"
".": "2.7.0"
}
+78
View File
@@ -1,5 +1,83 @@
# Changelog
## [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)
+9 -2
View File
@@ -103,6 +103,13 @@ 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.
If bare `npx` finds a project-local REA package, its dispatcher restarts setup
through `rea-agents@latest` before planning any changes. This prevents an older
local dependency from silently writing stale integration state. An already
published older release cannot contain that dispatcher protection; run
`npx -y rea-agents@latest setup` once to repair registrations created by one.
Intentional rollbacks 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
@@ -374,7 +381,7 @@ 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 |
@@ -530,7 +537,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@2.7.0", "mcp"]
}
}
}
+2 -2
View File
@@ -162,7 +162,7 @@ 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/الحدود الأصلية والتحقق منها، إسقاط رسم التطبيق، استيراد إعادة البناء من فك الترجمة، إعادة ربط الرموز، خطط ترابط وقت التشغيل ومقارنة الإصدارات |
@@ -183,7 +183,7 @@ rea uninstall --purge-data
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@2.5.0", "mcp"]
"args": ["-y", "rea-agents@2.7.0", "mcp"]
}
}
}
+2 -2
View File
@@ -152,7 +152,7 @@ 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 再マッピング、ランタイム相関計画、バージョン比較 |
@@ -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@2.7.0", "mcp"]
}
}
}
+2 -2
View File
@@ -152,7 +152,7 @@ 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 재매핑, 런타임 상관 계획, 버전 비교 |
@@ -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@2.7.0", "mcp"]
}
}
}
+2 -2
View File
@@ -152,7 +152,7 @@ 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 重映射、运行时关联计划与跨版本比较 |
@@ -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@2.7.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):
+38
View File
@@ -15,6 +15,24 @@ 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.
When bare `npx` resolves `rea-agents` from the current project's
`node_modules`, the selected dispatcher restarts setup through
`rea-agents@latest` before REA plans or writes configuration. This protects
current and future releases from silently persisting stale integration state.
Code in an already-published older release cannot acquire that check
retroactively; invoke `npx -y rea-agents@latest setup` once to repair any
registration it created.
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
@@ -145,6 +163,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
+11 -8
View File
@@ -2,7 +2,7 @@
"catalog_schema_version": 1,
"package": {
"name": "rea-agents",
"version": "2.5.0",
"version": "2.7.0",
"sdk": {
"server": "2.0.0-beta.4",
"client": "2.0.0-beta.4",
@@ -11,7 +11,7 @@
"skill_version": "23"
},
"tools": {
"total": 110,
"total": 111,
"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"
@@ -581,7 +582,7 @@
}
],
"cli": {
"primary_count": 66,
"primary_count": 67,
"commands": [
"analyze",
"analyze-javascript-application",
@@ -619,6 +620,7 @@
"inspect-managed-artifact",
"inspect-managed-members",
"inspect-managed-native-boundaries",
"inspect-native-api",
"inspect-plist",
"inspect-signature",
"inspect-web-page",
@@ -659,17 +661,18 @@
},
"runtime_catalog": {
"counts": {
"cli_commands": 66,
"mcp_tools": 110,
"cli_commands": 67,
"mcp_tools": 111,
"mcp_prompts": 6,
"mcp_resources": 2,
"mcp_resource_templates": 11
},
"digests": {
"tools_sha256": "9a033ddd40fcb8273365cc895bbf75d943bf1c5c8c2ee1eb9c4b3494064aaf58",
"tools_sha256": "5a88bf598a1e0fe705761db6af92f1ceabaf026cb6efa425e87b9633f1d7a3de",
"prompts_sha256": "f6da3ad38a1000c11364d1710f122ac4990551f04efd8c53ffb2e819b5cf5707",
"resources_sha256": "339a984ea8cd8be5f234adf24395bf0295258d990492005cf6fe1f293d12cd46",
"combined_sha256": "e8cac51f2f17a5b9a088c3ff773473c4897823401481fc0875a28fe7daf8eee0"
"combined_sha256": "861cd8618e793342daa8b149bc2944360d84b077fd19cf22f6e0a57b6b249c73",
"providers_sha256": "918ccd6b8ae442cd5844dcfde55086f458176241c8d166c1e9dd0f242ccd2bb0"
}
}
}
@@ -1,6 +1,6 @@
{
"schema_version": 1,
"manifest_id": "ecm_ee8e5f2f4ea090052051d2994cbff148d9c796e1bc28b2fdc145aa5fa48cd248",
"manifest_id": "ecm_24026e05aeb579814a3d44d04eb8914b1d4ce301d916536bc68a745418c5d892",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
@@ -14,7 +14,7 @@
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "6a910f067c946d545f5eb711eacbdf804839dbfb6b3b7b6ea91769be37ae47ce"
"sha256": "9bb090837e251fc7ff1c1e621b77230f54c88650efb6520aff74d33877b023d8"
}
],
"claims": [
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "rea-agents",
"version": "2.5.0",
"version": "2.7.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rea-agents",
"version": "2.5.0",
"version": "2.7.0",
"license": "MIT",
"dependencies": {
"@babel/parser": "8.0.4",
+6 -3
View File
@@ -1,6 +1,6 @@
{
"name": "rea-agents",
"version": "2.5.0",
"version": "2.7.0",
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
"license": "MIT",
"repository": {
@@ -43,6 +43,7 @@
"bridge/hopper_bridge.py",
"bridge/ghidra/ReaGhidraBridge.java",
"scripts/rea.mjs",
"scripts/package-runner-bootstrap.mjs",
"scripts/hopper-demo-x11.py",
"scripts/prepack.mjs",
"scripts/prepare.mjs",
@@ -54,6 +55,8 @@
"deps:check": "node scripts/check-dependency-install.mjs",
"metadata:generate": "node scripts/generate-package-metadata.mjs",
"metadata:check": "node scripts/generate-package-metadata.mjs --check",
"mcp-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",
"build": "tsc -p tsconfig.build.json",
"precheck": "npm run deps:check",
@@ -123,9 +126,9 @@
"evidence:check": "npm run build:cached && node scripts/generate-completion-ledger.mjs --check",
"docs:api": "typedoc",
"docs:api:cached": "turbo run docs:api",
"docs:generate": "npm run metadata:generate && npm run build:cached && node scripts/generate-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:generate": "npm run metadata:generate && npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs && node scripts/generate-skill-metadata.mjs && node scripts/generate-product-catalog.mjs && node scripts/generate-completion-ledger.mjs && npm run docs:api:cached && node scripts/generate-error-schema.mjs",
"docs:check": "turbo run docs:check:built",
"docs: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: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"
+153
View File
@@ -0,0 +1,153 @@
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { format } from "prettier";
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)), "..");
const sessionToolNames = new Set([
...TOOL_CONTRACTS.filter(({ kind }) => kind === "session").map(
({ name }) => name,
),
...MANAGED_WORKFLOW_TOOL_CONTRACTS.map(({ name }) => name),
]);
const catalog = TOOL_CONTRACTS.map((contract) => {
const options = toolRegistrationOptions(contract);
return {
name: contract.name,
analysisOperation: [
"official-proxy",
"enhanced",
"native-provider",
"artifact-provider",
"managed-provider",
].includes(contract.kind)
? contract.name
: null,
title: options.title,
description: options.description,
kind: contract.kind,
requiresSession: sessionToolNames.has(contract.name),
inputSchema: options.inputSchema["~standard"].jsonSchema.input({
target: "draft-2020-12",
}),
outputSchema: options.outputSchema["~standard"].jsonSchema.output({
target: "draft-2020-12",
}),
annotations: options.annotations,
effects: contract.effects,
};
});
const auxiliaryProviders = [
new ArtifactProvider(false, false),
new NativeMacOSProvider(),
new ManagedStaticProvider(),
].map((provider) => ({
identity: provider.identity(),
capabilities: provider.capabilities(),
}));
const payloadJson = JSON.stringify({ 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",
});
+11
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);
+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),
},
},
};
};
+55
View File
@@ -1,5 +1,60 @@
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.includes("Hopper bridge started request") ||
!messages.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,
+19
View File
@@ -0,0 +1,19 @@
export interface PackageRunnerSetupInput {
readonly args: readonly string[];
readonly environment: Readonly<Record<string, string | undefined>>;
readonly packageRoot: string;
readonly packageName: string;
}
export interface PackageRunnerSetupPlan {
readonly command: "npm";
readonly args: readonly string[];
}
export function packageRunnerSetupPlan(
input: PackageRunnerSetupInput,
): PackageRunnerSetupPlan | undefined;
export function runPackageRunnerSetupBootstrap(
input: PackageRunnerSetupInput,
): Promise<number | undefined>;
+72
View File
@@ -0,0 +1,72 @@
import { spawn } from "node:child_process";
import { resolve } from "node:path";
const BOOTSTRAP_MARKER = "REA_PACKAGE_RUNNER_BOOTSTRAPPED";
/**
* Plan a current-release setup bootstrap when bare npx selected a project-local
* package. Explicit `npm exec --package=<name>@<version>` rollbacks bypass it.
*/
export function packageRunnerSetupPlan(input) {
if (input.args[0] !== "setup") return undefined;
if (input.environment[BOOTSTRAP_MARKER] === "1") return undefined;
if (input.environment.npm_lifecycle_event !== "npx") return undefined;
const localPrefix = input.environment.npm_config_local_prefix;
if (localPrefix === undefined || localPrefix.length === 0) return undefined;
if (
!isPackageBelowLocalPrefix(
input.packageRoot,
localPrefix,
input.packageName,
)
)
return undefined;
const requestedPackage = input.environment.npm_config_package;
if (
requestedPackage !== undefined &&
isExactPackageVersion(requestedPackage, input.packageName)
)
return undefined;
return {
command: "npm",
args: [
"exec",
"--yes",
`--package=${input.packageName}@latest`,
"--",
"rea",
...input.args,
],
};
}
/** Execute a planned package-runner bootstrap and return its exit status. */
export async function runPackageRunnerSetupBootstrap(input) {
const plan = packageRunnerSetupPlan(input);
if (plan === undefined) return undefined;
return await new Promise((resolveExitCode) => {
const child = spawn(plan.command, plan.args, {
stdio: "inherit",
env: { ...input.environment, [BOOTSTRAP_MARKER]: "1" },
});
child.once("error", () => resolveExitCode(1));
child.once("exit", (code) => resolveExitCode(code ?? 1));
});
}
function isPackageBelowLocalPrefix(packageRoot, localPrefix, packageName) {
const expectedRoot = resolve(localPrefix, "node_modules", packageName);
return resolve(packageRoot) === expectedRoot;
}
function isExactPackageVersion(packageSpecifier, packageName) {
const version = packageSpecifier.slice(`${packageName}@`.length);
return (
packageSpecifier.startsWith(`${packageName}@`) &&
/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(version)
);
}
+11 -1
View File
@@ -4,6 +4,8 @@ import { access } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { runPackageRunnerSetupBootstrap } from "./package-runner-bootstrap.mjs";
// Route production MCP before importing Incur. Incur owns registration helpers
// such as `mcp add`, while only dist/main.js may serve the stdio tool catalog.
const args = process.argv.slice(2);
@@ -15,13 +17,21 @@ const isMcpMode =
args.length === 1 && (args[0] === "--mcp" || args[0] === "mcp");
const isMcpDoctorMode = args[0] === "mcp" && args[1] === "doctor";
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const bootstrapExitCode = await runPackageRunnerSetupBootstrap({
args,
environment: process.env,
packageRoot,
packageName: packageJson.name,
});
const runtimeFiles = isMcpMode
? ["dist/main.js"]
: isMcpDoctorMode
? ["dist/main.js", "dist/mcpDoctor.js"]
: ["dist/cli.js", "dist/cliOutput.js"];
if (!(await compiledRuntimeExists(runtimeFiles))) {
if (bootstrapExitCode !== undefined) {
process.exitCode = bootstrapExitCode;
} else if (!(await compiledRuntimeExists(runtimeFiles))) {
process.stderr.write(
`REA's compiled runtime is missing. Run \`npm ci\` in ${packageRoot} to install dependencies and build REA, then restart it. If this is an installed package, reinstall rea-agents.\n`,
);
+2 -1
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)), "..");
@@ -346,7 +347,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]),
+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" ||
+2 -1
View File
@@ -5,6 +5,7 @@ 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,
@@ -180,7 +181,7 @@ export async function verifyPackageMcp({
(
await execute(cli, ["mcp", "doctor", "--json"], {
env: environment,
timeout: 30_000,
timeout: MCP_STARTUP_POLICY.doctorDeadlineMs,
})
).stdout,
);
+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 });
+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;
+20 -25
View File
@@ -16,8 +16,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 +102,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"]) {
@@ -312,7 +291,11 @@ try {
);
}
const options = { timeout };
const progressUpdates = [];
const options = {
timeout,
onprogress: (update) => progressUpdates.push(update),
};
const fullSessionStatus = () =>
client.callTool(
{ name: "binary_session", arguments: { detail: "full" } },
@@ -343,6 +326,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 +428,7 @@ try {
);
}
const diagnosticCount = requireSafeDiagnostics(stderrChunks);
requireBridgeDiagnostic(stderrChunks);
const closed = await client.callTool(
{ name: "close_binary", arguments: {} },
@@ -460,6 +454,7 @@ try {
bundledMcpRunning,
stderrBytes,
diagnosticCount,
bridgeProgressUpdates: progressUpdates.length,
dynamicSession: true,
providerBinding,
targets: [targetA, targetB, largeTarget],
+2 -2
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: 111
catalog_digest: "861cd8618e793342daa8b149bc2944360d84b077fd19cf22f6e0a57b6b249c73"
---
# REA
+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. */
+10 -4
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,
),
);
/**
@@ -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 (
+4 -3
View File
@@ -1,8 +1,9 @@
import { z } from "zod";
import { TOOL_CONTRACTS } from "../contracts/toolContracts.js";
import type { ClientFeatureAvailability } 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";
const statusSchema = z.object({
open: z.boolean(),
@@ -28,7 +29,6 @@ type ToolAvailabilityReason =
| "unsupported_host"
| "policy_disabled";
type ToolKind = (typeof TOOL_CONTRACTS)[number]["kind"];
type ProviderDescriptor = {
readonly available: boolean;
readonly reason: string | null;
@@ -102,6 +102,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",
@@ -127,7 +128,7 @@ export const buildCapabilityInventory = (
},
]),
);
return TOOL_CONTRACTS.map((contract) => {
return GENERATED_MCP_TOOL_CATALOG.map((contract) => {
const availability = availabilityFor({
name: contract.name,
kind: contract.kind,
+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 &&
+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);
+51 -7
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) =>
@@ -181,6 +183,30 @@ export class EnhancedTools {
);
}
#executeFunctionAnalysis(
name: "analyze_function" | "inspect_native_api",
input: unknown,
signal?: AbortSignal,
): EnhancedResult {
const parsed = enhancedInputSchemas[name].safeParse(input);
if (!parsed.success) return invalidEnhancedInput(name, parsed.error);
if (name === "analyze_function")
return this.executeValidated(
{
name,
input: enhancedInputSchemas.analyze_function.parse(parsed.data),
},
signal,
);
return this.executeValidated(
{
name,
input: enhancedInputSchemas.inspect_native_api.parse(parsed.data),
},
signal,
);
}
async #analyzeFunction(
input: Readonly<Record<string, JsonValue>>,
signal?: AbortSignal,
@@ -189,6 +215,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
+61 -18
View File
@@ -3,6 +3,7 @@ import canonicalize from "canonicalize";
import {
createEvidenceBundle,
parseEvidenceBundle,
validateResidualUnknownAddition,
type EvidenceBundle,
} from "../domain/evidenceBundle.js";
import {
@@ -216,8 +217,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 +224,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 +323,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>,
+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,
+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;
}
}
+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)];
};
+4 -3
View File
@@ -6,6 +6,7 @@ import writeFileAtomic from "write-file-atomic";
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
import { PRODUCT_IDENTITY } from "../identity.js";
import { MCP_STARTUP_POLICY } from "../mcpStartupPolicy.js";
import { resolveClientConfigTransactionPath } from "./ClientConfigPath.js";
import type {
ClientConfigurationInspection,
@@ -21,8 +22,6 @@ const defaultCommand = (): readonly string[] => [
"mcp",
];
const CODEX_STARTUP_TIMEOUT_SECONDS = 30;
/** Back up, atomically update, and semantically read back one JSON MCP configuration. */
export const configureJsonClient = async (
client: SetupClient,
@@ -299,7 +298,9 @@ const clientConfigurationDesired = (
command: command[0] ?? PRODUCT_IDENTITY.cliBinary,
args: command.slice(1),
...(client.name === "codex"
? { startup_timeout_sec: CODEX_STARTUP_TIMEOUT_SECONDS }
? {
startup_timeout_sec: MCP_STARTUP_POLICY.codexStartupTimeoutSeconds,
}
: {}),
...(Object.keys(environment).length === 0 ? {} : { env: environment }),
};
+38 -9
View File
@@ -2,11 +2,10 @@ import type { AppConfig } from "../config.js";
import { BinarySession } from "./BinarySession.js";
import { HopperProvider } from "../hopper/HopperProvider.js";
import { GhidraProvider } from "../ghidra/GhidraProvider.js";
import { NativeMacOSProvider } from "../native/NativeMacOSProvider.js";
import { ArtifactProvider } from "../artifacts/ArtifactProvider.js";
import { ManagedStaticProvider } from "../dotnet/ManagedStaticProvider.js";
import { silentLogger, type Logger } from "../logger.js";
import { GENERATED_AUXILIARY_PROVIDERS } from "../generatedMcpToolCatalog.js";
import { AnalysisProviderRegistry } from "./AnalysisProviderRegistry.js";
import { LazyAnalysisProvider } from "./LazyAnalysisProvider.js";
import { SessionProviderRouter } from "./SessionProviderRouter.js";
/**
@@ -20,16 +19,46 @@ export const createBinarySession = (
): BinarySession => {
const hopper = new HopperProvider(config, logger);
const ghidra = new GhidraProvider(config, logger);
const auxiliary = new Map(
GENERATED_AUXILIARY_PROVIDERS.map((provider) => [
provider.identity.id,
provider,
]),
);
const lazyProvider = (
id: string,
load: ConstructorParameters<typeof LazyAnalysisProvider>[0]["load"],
) => {
const generated = auxiliary.get(id);
if (generated === undefined)
throw new TypeError(`Missing generated provider metadata for ${id}`);
return new LazyAnalysisProvider({ ...generated, load });
};
return new BinarySession(
SessionProviderRouter.selectable(
new AnalysisProviderRegistry([hopper, ghidra], config.analysisProvider),
[
new ArtifactProvider(
config.artifactNativeMountEnabled,
config.artifactIntegrityContinueEnabled,
),
new NativeMacOSProvider(),
new ManagedStaticProvider(),
lazyProvider("rea-artifact-graph", async () => {
const { ArtifactProvider } = await import(
"../artifacts/ArtifactProvider.js"
);
return new ArtifactProvider(
config.artifactNativeMountEnabled,
config.artifactIntegrityContinueEnabled,
);
}),
lazyProvider("native-macos", async () => {
const { NativeMacOSProvider } = await import(
"../native/NativeMacOSProvider.js"
);
return new NativeMacOSProvider();
}),
lazyProvider("rea-dotnet-static", async () => {
const { ManagedStaticProvider } = await import(
"../dotnet/ManagedStaticProvider.js"
);
return new ManagedStaticProvider();
}),
],
),
);
@@ -29,9 +29,19 @@ import type {
BrowserScenarioSessionPort,
} from "./BrowserScenarioSessionPort.js";
import { BrowserScenarioCaptureBudget } from "./PlaywrightScenarioArtifacts.js";
import { PlaywrightScenarioSessionFactory } from "./PlaywrightScenarioSession.js";
const OPERATION = "capture_browser_scenario" as const;
let defaultFactory: Promise<BrowserScenarioSessionFactory> | undefined;
const lazyPlaywrightFactory: BrowserScenarioSessionFactory = {
async open(scenario, options) {
defaultFactory ??= import("./PlaywrightScenarioSession.js").then(
({ PlaywrightScenarioSessionFactory }) =>
new PlaywrightScenarioSessionFactory(),
);
return (await defaultFactory).open(scenario, options);
},
};
export const PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY: ProviderIdentity =
Object.freeze({
@@ -353,7 +363,7 @@ export class PlaywrightBrowserScenarioProvider
implements BrowserScenarioCapturePort
{
constructor(
private readonly factory: BrowserScenarioSessionFactory = new PlaywrightScenarioSessionFactory(),
private readonly factory: BrowserScenarioSessionFactory = lazyPlaywrightFactory,
) {}
identity(): ProviderIdentity {
+5 -3
View File
@@ -90,14 +90,16 @@ const registerExtractionCommand = (cli: CliInstance, logger: Logger): void => {
args: z.object({
path: z.string().describe("Application or package path"),
outputRoot: z.string().describe("Absent absolute output root"),
}),
options: z.object({
occurrenceIds: z
.array(z.string().regex(/^occ_[a-f0-9]{64}$/u))
.min(1)
.max(500)
.describe("Exact artifact occurrence IDs selected for extraction"),
}),
alias: { outputRoot: "output-root", occurrenceIds: "occurrence-ids" },
run: ({ args }) =>
alias: { occurrenceIds: "occurrence-ids" },
run: ({ args, options }) =>
logCliCommand(logger, "extract-artifact", () =>
runProviderAnalysis(
args.path,
@@ -105,7 +107,7 @@ const registerExtractionCommand = (cli: CliInstance, logger: Logger): void => {
{
approved: true,
output_root: args.outputRoot,
occurrence_ids: args.occurrenceIds,
occurrence_ids: options.occurrenceIds,
},
logger,
),
+50
View File
@@ -16,6 +16,7 @@ export const registerCoreAnalysisCommands = (
): void => {
registerCoreCommands(cli, logger);
registerFunctionCommand(cli, logger);
registerNativeApiCommand(cli, logger);
registerInstructionsCommand(cli, logger);
registerSearchCommand(cli, logger);
registerXrefsCommand(cli, logger);
@@ -286,6 +287,55 @@ const registerFunctionCommand = (cli: CliInstance, logger: Logger): void => {
});
};
const registerNativeApiCommand = (cli: CliInstance, logger: Logger): void => {
cli.command(CLI_COMMANDS.inspectNativeApi, {
description: "Reconstruct one native function API boundary with evidence",
args: z.object({
path: z.string().describe("App or program path"),
address: z.string().describe("Procedure name or address"),
}),
options: z.object({
maxPseudocodeChars: z
.number()
.int()
.min(1)
.max(100_000)
.default(20_000)
.describe("Maximum pseudocode characters to inspect"),
maxInstructions: z
.number()
.int()
.min(1)
.max(5_000)
.default(500)
.describe("Maximum assembly instructions to inspect"),
snapshot: z
.string()
.min(1)
.optional()
.describe("Load and update a local analysis snapshot"),
provider: providerSelectionOption,
}),
alias: {
maxPseudocodeChars: "max-pseudocode-chars",
maxInstructions: "max-instructions",
},
run: ({ args, options }) =>
logCliCommand(logger, "inspect-native-api", () =>
runDirectAnalysis(
args.path,
"inspect_native_api",
{
procedure: args.address,
max_pseudocode_chars: options.maxPseudocodeChars,
max_instructions: options.maxInstructions,
},
directAnalysisOptions(logger, options.snapshot, options.provider),
),
),
});
};
const registerInstructionsCommand = (
cli: CliInstance,
logger: Logger,
+1
View File
@@ -12,6 +12,7 @@ export const CLI_COMMANDS = Object.freeze({
capabilities: "capabilities",
providers: "providers",
function: "function",
inspectNativeApi: "inspect-native-api",
instructions: "instructions",
search: "search",
importReferenceSource: "import-reference-source",
+12
View File
@@ -63,6 +63,17 @@ export const enhancedInputSchemas = {
basic_blocks: 0,
}),
}),
inspect_native_api: z.object({
procedure: z.string().describe("A procedure name or address"),
max_pseudocode_chars: z.number().int().min(1).max(100_000).default(20_000),
max_instructions: z.number().int().min(1).max(5_000).default(500),
unknown_registry_approved: z
.literal(true)
.optional()
.describe(
"Explicit approval to record unsupported native API branches durably",
),
}),
trace_feature: traceLiteralInputSchema,
find_code_for_string: traceLiteralInputSchema,
trace_call_path: z.object({
@@ -95,6 +106,7 @@ export const enhancedToolNameSchema = z.enum([
"find_xrefs_to_name",
"binary_overview",
"analyze_function",
"inspect_native_api",
"trace_feature",
"find_code_for_string",
"trace_call_path",
@@ -0,0 +1,46 @@
import { jsonObjectSchema } from "../domain/jsonValue.js";
import { enhancedInputSchemas } from "./enhancedInputs.js";
import { TOOL_EXAMPLE_OVERRIDES } from "./toolContractExamples.js";
import type { ToolContract } from "./toolContractTypes.js";
import { toolContractMetadata } from "./toolEffects.js";
import { enhancedOutputSchemas } from "./toolOutputSchemas.js";
type FunctionWorkflowName = "analyze_function" | "inspect_native_api";
const functionWorkflow = <Name extends FunctionWorkflowName>(
name: Name,
description: string,
): ToolContract<Name> => {
const inputSchema = enhancedInputSchemas[name];
const outputSchema = enhancedOutputSchemas[name];
if (outputSchema === undefined)
throw new Error(`Missing function workflow output schema for ${name}`);
return {
name,
...toolContractMetadata(name),
description,
kind: "enhanced",
inputSchema,
outputSchema,
examples: [
{
title: `Example ${name.replaceAll("_", " ")} request`,
input: jsonObjectSchema.parse(
inputSchema.parse(TOOL_EXAMPLE_OVERRIDES[name] ?? {}),
),
},
],
};
};
/** Function dossier and native API reconstruction workflow contracts. */
export const FUNCTION_WORKFLOW_TOOL_CONTRACTS = [
functionWorkflow(
"analyze_function",
"Preferred bounded analysis for one procedure symbol or address. Returns identity, provider-specific pseudocode, optional assembly, comments, calls, typed-or-explicitly-unavailable references, referenced strings/names, and local CFG blocks with exact truncation metadata. Providers with a structured decompiler model also expose native API boundary types, confidence, evidence, jump-table data/target addresses, and explicit decompiler-artifact labels.",
),
functionWorkflow(
"inspect_native_api",
"Reconstruct one native function boundary through inspectable substeps. Returns structured inferred return/parameter types with confidence and evidence, jump-table dispatch/data/target mappings, explicit decompiler-artifact labels, unsupported branches, and residual unknowns.",
),
] as const satisfies readonly ToolContract[];
+1 -1
View File
@@ -1,4 +1,4 @@
import { TOOL_CONTRACTS } from "./toolContracts.js";
import type { TOOL_CONTRACTS } from "./toolContracts.js";
type ToolName = (typeof TOOL_CONTRACTS)[number]["name"];
+1
View File
@@ -35,6 +35,7 @@ export const TOOL_EXAMPLE_OVERRIDES: Readonly<
get_call_graph: { address: "0x1000" },
find_xrefs_to_name: { name: "malloc" },
analyze_function: { procedure: "main" },
inspect_native_api: { procedure: "main" },
trace_feature: { query: "license" },
find_code_for_string: { query: "authorization failed" },
trace_call_path: { start: "0x1000", goal: "0x1100" },
+1 -1
View File
@@ -32,7 +32,7 @@ export const TOOL_KINDS = [
] as const;
/** Adapter family responsible for implementing a public MCP tool. */
type ToolKind = (typeof TOOL_KINDS)[number];
export type ToolKind = (typeof TOOL_KINDS)[number];
/** Single source of truth for a public MCP tool. */
export interface ToolContract<Name extends string = string> {
+2 -5
View File
@@ -41,6 +41,7 @@ import { binarySessionInputSchema } from "./sessionStatusContract.js";
import { functionInstructionInputSchema } from "./functionInstructionContract.js";
import { HOPPER_MEMORY_TOOL_DEFINITIONS } from "./hopperMemoryContracts.js";
import { analysisSearchInput } from "./analysisSearchContract.js";
import { FUNCTION_WORKFLOW_TOOL_CONTRACTS } from "./functionWorkflowToolContracts.js";
export { binarySessionInputSchema } from "./sessionStatusContract.js";
@@ -354,11 +355,7 @@ export const ENHANCED_TOOL_CONTRACTS = [
"Use immediately after opening a target to summarize document, exhaustive procedure/string counts, and a bounded segment sample. detail controls segment fields and limit controls only the returned segment sample.",
enhancedInputSchemas.binary_overview,
),
enhanced(
"analyze_function",
"Preferred bounded analysis for one procedure symbol or address. Returns identity, provider-specific pseudocode, optional assembly, comments, calls, typed-or-explicitly-unavailable references, referenced strings/names, and local CFG blocks with exact truncation metadata.",
enhancedInputSchemas.analyze_function,
),
...FUNCTION_WORKFLOW_TOOL_CONTRACTS,
enhanced(
"trace_feature",
"Trace a bounded literal feature query through matching strings and procedures, xrefs, and truthful containing-procedure resolution. Returns the operation budget, truncation, and residual unknowns; unknown_registry_approved: true records them durably without inferring reference kinds.",
+1
View File
@@ -89,6 +89,7 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
find_xrefs_to_name: evidence,
binary_overview: evidence,
analyze_function: evidence,
inspect_native_api: effects({ mutatesSession: true, idempotent: false }),
trace_feature: effects({ mutatesSession: true, idempotent: false }),
find_code_for_string: effects({ mutatesSession: true, idempotent: false }),
trace_call_path: effects({ mutatesSession: true, idempotent: false }),
+2
View File
@@ -10,6 +10,7 @@ import {
functionInstructionWindowSchema,
referenceKindSchema,
} from "../domain/hopperValues.js";
import { nativeApiInspectionResultSchema } from "../domain/nativeApiBoundary.js";
import {
demangleSwiftSchema,
inspectMachoSchema,
@@ -272,6 +273,7 @@ export const enhancedOutputSchemas: Readonly<Record<string, z.ZodObject>> = {
}),
),
analyze_function: functionDossierOutput,
inspect_native_api: resultOf(nativeApiInspectionResultSchema),
trace_feature: literalTraceOutput,
find_code_for_string: literalTraceOutput,
trace_call_path: callPathTraceOutput,
+206
View File
@@ -0,0 +1,206 @@
import { z } from "zod";
/** Supported managed-runtime bytecode families. */
export const bytecodeFamilySchema = z.enum(["jvm", "python"]);
export type BytecodeFamily = z.infer<typeof bytecodeFamilySchema>;
/** Classification of bytecode provenance. */
export const bytecodeProvenanceSchema = z.enum([
"application",
"generated",
"vendored",
"bundled",
"standard_library",
]);
export type BytecodeProvenance = z.infer<typeof bytecodeProvenanceSchema>;
/** A bytecode symbol discovered through static analysis. */
export const bytecodeSymbolSchema = z.strictObject({
/** Fully qualified name. */
name: z.string().min(1),
/** Raw bytecode location or offset. */
raw_location: z.string().min(1),
/** Normalized symbol type. */
kind: z.enum([
"class",
"interface",
"method",
"field",
"function",
"module",
"variable",
]),
/** Bytecode version if known. */
bytecode_version: z.number().int().nullable(),
/** Provenance classification. */
provenance: bytecodeProvenanceSchema,
/** Whether the symbol is accessible (public). */
is_public: z.boolean().default(false),
});
export type BytecodeSymbol = z.infer<typeof bytecodeSymbolSchema>;
/** A discovered bytecode artifact (class file, wheel, etc). */
export const bytecodeArtifactSchema = z.strictObject({
/** Artifact path relative to the analysis root. */
path: z.string().min(1),
/** Bytecode family. */
family: bytecodeFamilySchema,
/** Artifact format. */
format: z.enum(["class", "jar", "war", "ear", "pyc", "pyo", "whl", "zipapp"]),
/** Size in bytes. */
size: z.number().int().nonnegative(),
/** SHA-256 digest. */
digest: z.string().regex(/^[a-f0-9]{64}$/u),
/** Discovered symbols. */
symbols: z.array(bytecodeSymbolSchema).default([]),
/** Whether this is a standard library artifact. */
is_standard_library: z.boolean().default(false),
/** Whether this is a generated artifact. */
is_generated: z.boolean().default(false),
/** Whether this is a vendored artifact. */
is_vendored: z.boolean().default(false),
});
export type BytecodeArtifact = z.infer<typeof bytecodeArtifactSchema>;
/** Static bytecode analysis result. */
export const bytecodeAnalysisSchema = z.strictObject({
/** Bytecode family. */
family: bytecodeFamilySchema,
/** All discovered artifacts. */
artifacts: z.array(bytecodeArtifactSchema).min(0).max(10_000),
/** Total symbols across all artifacts. */
total_symbols: z.number().int().nonnegative(),
/** Symbols classified as application code. */
application_symbols: z.number().int().nonnegative(),
/** Symbols classified as generated code. */
generated_symbols: z.number().int().nonnegative(),
/** Symbols classified as vendored code. */
vendored_symbols: z.number().int().nonnegative(),
/** Symbols classified as standard library. */
standard_library_symbols: z.number().int().nonnegative(),
});
export type BytecodeAnalysis = z.infer<typeof bytecodeAnalysisSchema>;
/** Detect bytecode family from file extension. */
export function detectBytecodeFamily(path: string): BytecodeFamily | null {
const lower = path.toLowerCase();
if (
lower.endsWith(".class") ||
lower.endsWith(".jar") ||
lower.endsWith(".war") ||
lower.endsWith(".ear")
) {
return "jvm";
}
if (
lower.endsWith(".pyc") ||
lower.endsWith(".pyo") ||
lower.endsWith(".whl") ||
lower.endsWith(".pyz") ||
lower.endsWith(".zipapp")
) {
return "python";
}
return null;
}
/** Detect artifact format from file path. */
export function detectArtifactFormat(
path: string,
): `class` | `jar` | `war` | `ear` | `pyc` | `pyo` | `whl` | `zipapp` | null {
const lower = path.toLowerCase();
if (lower.endsWith(".class")) return "class";
if (lower.endsWith(".jar")) return "jar";
if (lower.endsWith(".war")) return "war";
if (lower.endsWith(".ear")) return "ear";
if (lower.endsWith(".pyc")) return "pyc";
if (lower.endsWith(".pyo")) return "pyo";
if (lower.endsWith(".whl")) return "whl";
if (lower.endsWith(".pyz") || lower.endsWith(".zipapp")) return "zipapp";
return null;
}
/** Classify bytecode symbol provenance. */
export function classifyProvenance(
path: string,
symbolName: string,
): BytecodeProvenance {
const lower = path.toLowerCase();
// Standard library paths
if (
lower.includes("/stdlib/") ||
lower.includes("/site-packages/") ||
lower.includes("java/") ||
lower.includes("javax/") ||
lower.includes("sun/") ||
symbolName.startsWith("java.") ||
symbolName.startsWith("javax.") ||
symbolName.startsWith("sun.") ||
symbolName.startsWith("__")
) {
return "standard_library";
}
// Generated code
if (
lower.includes("/generated/") ||
lower.includes("_generated") ||
lower.includes("/build/") ||
lower.includes("/target/") ||
lower.includes("/dist-packages/") ||
lower.includes("/__pycache__/") ||
symbolName.includes("Generated") ||
symbolName.includes("_generated")
) {
return "generated";
}
// Vendored code
if (
lower.includes("/vendor/") ||
lower.includes("/vendored/") ||
lower.includes("/third_party/") ||
lower.includes("/3rdparty/") ||
lower.includes("/node_modules/") ||
lower.includes("/vendor/")
) {
return "vendored";
}
// Bundled code
if (
lower.includes("/bundle/") ||
lower.includes("/bundled/") ||
lower.includes(".jar!") ||
lower.includes(".war!")
) {
return "bundled";
}
return "application";
}
/** Analyze a set of bytecode artifacts and produce a summary. */
export function analyzeBytecodeArtifacts(
artifacts: readonly BytecodeArtifact[],
family: BytecodeFamily,
): BytecodeAnalysis {
const applicationSymbols = artifacts
.flatMap((a) => a.symbols)
.filter((s) => s.provenance === "application").length;
const generatedSymbols = artifacts
.flatMap((a) => a.symbols)
.filter((s) => s.provenance === "generated").length;
const vendoredSymbols = artifacts
.flatMap((a) => a.symbols)
.filter((s) => s.provenance === "vendored").length;
const standardLibrarySymbols = artifacts
.flatMap((a) => a.symbols)
.filter((s) => s.provenance === "standard_library").length;
return {
family,
artifacts: artifacts.slice(0, 10_000),
total_symbols: artifacts.reduce((sum, a) => sum + a.symbols.length, 0),
application_symbols: applicationSymbols,
generated_symbols: generatedSymbols,
vendored_symbols: vendoredSymbols,
standard_library_symbols: standardLibrarySymbols,
};
}
+218
View File
@@ -0,0 +1,218 @@
import { createHash } from "node:crypto";
import canonicalize from "canonicalize";
import { z } from "zod";
import { evidenceEnvelopeSchema } from "./evidence.js";
import { evidenceBundleSchema } from "./evidenceBundle.js";
/** Schema version for the conformance package format. */
export const CONFORMANCE_PACKAGE_VERSION = 1;
/** A stable, path-independent identifier for a conformance package. */
export const conformancePackageIdSchema = z
.string()
.regex(/^cp_[a-f0-9]{64}$/u);
/** Identifier for a single scenario within a package. */
export const scenarioIdSchema = z
.string()
.regex(/^[A-Za-z][A-Za-z0-9._-]{0,99}$/u);
/** Deterministic manifest for a scenario fixture. */
export const scenarioManifestSchema = z.strictObject({
scenario_id: scenarioIdSchema,
name: z.string().min(1),
description: z.string().min(1),
/** Source-owned fixture path relative to the repository root. */
fixture_path: z.string().min(1),
/** Expected exit code or null if unspecified. */
expected_exit_code: z.number().int().nullable(),
/** Expected output patterns that must appear in the capture. */
expected_patterns: z.array(z.string()).default([]),
});
/** Replay plan describing how a scenario is replayed deterministically. */
export const replayPlanSchema = z.strictObject({
scenario_id: scenarioIdSchema,
/** Ordered steps to execute during replay. */
steps: z
.array(
z.strictObject({
step_id: z.string().min(1),
action: z.string().min(1),
arguments: z.array(z.string()).default([]),
timeout_ms: z.number().int().positive(),
}),
)
.min(1)
.max(100),
/** Environment variables to set (never inherit host paths). */
environment: z.record(z.string(), z.string()).default({}),
});
/** Shim plan for intercepting observable effects. */
export const shimPlanSchema = z.strictObject({
scenario_id: scenarioIdSchema,
/** Shims to install, each intercepting a named effect. */
shims: z
.array(
z.strictObject({
shim_id: z.string().min(1),
kind: z.enum(["filesystem", "network", "process", "signal"]),
target: z.string().min(1),
policy: z.enum(["observe", "allow", "block", "emulate"]),
}),
)
.min(0)
.max(50),
});
/** Expected evidence for a scenario, checked against the captured run. */
export const expectedEvidenceSchema = z.strictObject({
scenario_id: scenarioIdSchema,
/** Expected evidence envelopes. */
envelopes: z.array(evidenceEnvelopeSchema).min(0).max(100),
/** Expected evidence bundle. */
bundle: evidenceBundleSchema.nullable(),
/** Required dimensions that must be present in the evidence. */
required_dimensions: z.array(z.string().min(1)).default([]),
});
/** Verifier contract describing how to validate conformance. */
export const verifierContractSchema = z.strictObject({
scenario_id: scenarioIdSchema,
/** Dimensions to verify. */
dimensions: z
.array(
z.strictObject({
name: z.string().min(1),
required: z.boolean().default(true),
comparison: z.enum(["exact", "semantic", "fuzzy"]),
}),
)
.min(1)
.max(50),
/** Tolerance for timing differences in milliseconds. */
timing_tolerance_ms: z.number().int().nonnegative().default(0),
});
/** Top-level conformance package manifest. */
export const conformancePackageSchema = z.strictObject({
schema_version: z.literal(CONFORMANCE_PACKAGE_VERSION),
package_id: conformancePackageIdSchema,
name: z.string().min(1),
description: z.string().min(1),
created_at: z.string().datetime(),
/** Scenario manifests. */
scenarios: z.array(scenarioManifestSchema).min(1).max(100),
/** Replay plans keyed by scenario_id. */
replay_plans: z.array(replayPlanSchema).min(1).max(100),
/** Shim plans keyed by scenario_id. */
shim_plans: z.array(shimPlanSchema).default([]),
/** Expected evidence for each scenario. */
expected_evidence: z.array(expectedEvidenceSchema).min(1).max(100),
/** Verifier contracts for each scenario. */
verifier_contracts: z.array(verifierContractSchema).min(1).max(100),
});
export type ConformancePackage = z.infer<typeof conformancePackageSchema>;
export type ScenarioManifest = z.infer<typeof scenarioManifestSchema>;
export type ReplayPlan = z.infer<typeof replayPlanSchema>;
export type ShimPlan = z.infer<typeof shimPlanSchema>;
export type ExpectedEvidence = z.infer<typeof expectedEvidenceSchema>;
export type VerifierContract = z.infer<typeof verifierContractSchema>;
/** Error from validation or package operations. */
export type ConformancePackageError =
| { kind: "invalid_package"; message: string }
| { kind: "scenario_not_found"; scenario_id: string }
| { kind: "duplicate_scenario"; scenario_id: string };
/** Result type for package operations. */
export type ConformancePackageResult<T> =
| { ok: true; value: T }
| { ok: false; error: ConformancePackageError };
/** Validate a conformance package and cross-check internal references. */
export function validateConformancePackage(
input: unknown,
): ConformancePackageResult<ConformancePackage> {
const parsed = conformancePackageSchema.safeParse(input);
if (!parsed.success)
return {
ok: false,
error: {
kind: "invalid_package",
message: parsed.error.issues[0]?.message ?? "invalid package",
},
};
const pkg = parsed.data;
// Check for duplicate scenario IDs
const seen = new Set<string>();
for (const s of pkg.scenarios) {
if (seen.has(s.scenario_id))
return {
ok: false,
error: { kind: "duplicate_scenario", scenario_id: s.scenario_id },
};
seen.add(s.scenario_id);
}
// Check that replay plans reference existing scenarios
for (const rp of pkg.replay_plans) {
if (!seen.has(rp.scenario_id))
return {
ok: false,
error: {
kind: "scenario_not_found",
scenario_id: rp.scenario_id,
},
};
}
// Check that expected evidence references existing scenarios
for (const ee of pkg.expected_evidence) {
if (!seen.has(ee.scenario_id))
return {
ok: false,
error: {
kind: "scenario_not_found",
scenario_id: ee.scenario_id,
},
};
}
// Check that verifier contracts reference existing scenarios
for (const vc of pkg.verifier_contracts) {
if (!seen.has(vc.scenario_id))
return {
ok: false,
error: {
kind: "scenario_not_found",
scenario_id: vc.scenario_id,
},
};
}
return { ok: true, value: pkg };
}
/** Compute a deterministic package ID from the canonical JSON. */
export function computePackageId(
pkg: Omit<ConformancePackage, "package_id">,
): string {
const json = canonicalize(pkg);
if (!json) throw new Error("failed to canonicalize package");
return `cp_${createHash("sha256").update(json).digest("hex")}`;
}
/** Create a conformance package with an auto-computed package_id. */
export function createConformancePackage(
pkg: Omit<ConformancePackage, "package_id">,
): ConformancePackage {
const package_id = computePackageId(pkg);
return { package_id, ...pkg };
}
+138
View File
@@ -0,0 +1,138 @@
import { z } from "zod";
import {
conformancePackageSchema,
type ConformancePackage,
} from "./conformancePackage.js";
import { trustGateResultSchema } from "./conformanceTrustGate.js";
/** Result of a single scenario replay. */
export const scenarioReplayResultSchema = z.strictObject({
scenario_id: z.string().min(1),
status: z.enum(["pass", "fail", "error", "skipped"]),
exit_code: z.number().int().nullable(),
duration_ms: z.number().int().nonnegative(),
output: z.string().default(""),
error: z.string().nullable(),
});
/** Result of replaying an entire conformance package. */
export const packageReplayResultSchema = z.strictObject({
package_id: z.string().min(1),
total_scenarios: z.number().int().positive(),
passed: z.number().int().nonnegative(),
failed: z.number().int().nonnegative(),
errored: z.number().int().nonnegative(),
skipped: z.number().int().nonnegative(),
scenario_results: z.array(scenarioReplayResultSchema),
trust_gate_results: z.array(trustGateResultSchema),
drift_detected: z.boolean(),
first_drift: z
.strictObject({
scenario_id: z.string().min(1),
dimension: z.string().min(1),
message: z.string(),
})
.nullable(),
});
export type ScenarioReplayResult = z.infer<typeof scenarioReplayResultSchema>;
export type PackageReplayResult = z.infer<typeof packageReplayResultSchema>;
/** Callback interface for replaying a scenario. */
export interface ScenarioRunner {
(scenarioId: string, fixturePath: string): Promise<ScenarioReplayResult>;
}
/**
* Default no-op scenario runner that skips all scenarios.
* Real CI replays inject an actual runner.
*/
async function defaultRunner(): Promise<ScenarioReplayResult> {
return {
scenario_id: "unknown",
status: "skipped",
exit_code: null,
duration_ms: 0,
output: "",
error: "no runner provided",
};
}
/**
* Replay a conformance package from a clean checkout.
* Runs each scenario, evaluates trust gates, and detects drift
* between the captured run and the committed package.
*/
export async function replayConformancePackage(
packageInput: unknown,
actualEvidence: Record<string, Record<string, unknown>>,
runner: ScenarioRunner = defaultRunner,
options: { truncated?: boolean } = {},
): Promise<PackageReplayResult> {
const parsed = conformancePackageSchema.safeParse(packageInput);
if (!parsed.success)
throw new TypeError(
`invalid conformance package: ${parsed.error.issues[0]?.message ?? "unknown"}`,
);
const pkg: ConformancePackage = parsed.data;
const scenarioResults: ScenarioReplayResult[] = [];
let passed = 0;
let failed = 0;
let errored = 0;
let skipped = 0;
for (const scenario of pkg.scenarios) {
const result = await runner(scenario.scenario_id, scenario.fixture_path);
scenarioResults.push(result);
switch (result.status) {
case "pass":
passed++;
break;
case "fail":
failed++;
break;
case "error":
errored++;
break;
case "skipped":
skipped++;
break;
}
}
const trustGateResults = evaluatePackageTrustGates(
pkg,
actualEvidence,
options,
);
const driftGates = trustGateResults.filter((r) => r.verdict === "fail");
const drift_detected = driftGates.length > 0;
const first_drift_gate = driftGates[0];
const first_drift = first_drift_gate?.first_divergence
? {
scenario_id: first_drift_gate.scenario_id,
dimension: first_drift_gate.first_divergence.dimension,
message: first_drift_gate.first_divergence.message,
}
: null;
return {
package_id: pkg.package_id,
total_scenarios: pkg.scenarios.length,
passed,
failed,
errored,
skipped,
scenario_results: scenarioResults,
trust_gate_results: trustGateResults,
drift_detected,
first_drift,
};
}
/** Import trust gate evaluator for replay. */
import { evaluatePackageTrustGates } from "./conformanceTrustGate.js";
+246
View File
@@ -0,0 +1,246 @@
import { z } from "zod";
import type {
ConformancePackage,
VerifierContract,
} from "./conformancePackage.js";
/** Result of comparing a single dimension. */
export const dimensionResultSchema = z.strictObject({
name: z.string().min(1),
status: z.enum(["match", "mismatch", "unknown", "truncated"]),
message: z.string().default(""),
evidence_ids: z.array(z.string()).default([]),
});
/** Result of evaluating a trust gate for one scenario. */
export const trustGateResultSchema = z.strictObject({
scenario_id: z.string().min(1),
verdict: z.enum(["pass", "fail", "unknown"]),
dimension_results: z.array(dimensionResultSchema),
first_divergence: z
.strictObject({
dimension: z.string().min(1),
evidence_id: z.string().min(1),
message: z.string(),
})
.nullable(),
});
export type DimensionResult = z.infer<typeof dimensionResultSchema>;
export type TrustGateResult = z.infer<typeof trustGateResultSchema>;
/** Dimension that is volatile due to timing or normalization. */
const VOLATILE_DIMENSIONS = new Set([
"timing",
"timestamp",
"pid",
"ppid",
"duration_ms",
"created_at",
"updated_at",
]);
/** Dimension that carries semantic content. */
const SEMANTIC_DIMENSIONS = new Set([
"exit_code",
"stdout",
"stderr",
"filesystem",
"process_tree",
"shim_events",
"protocol_events",
"event_journal",
]);
/** Check if a dimension name is volatile (timing/normalization noise). */
export function isVolatileDimension(name: string): boolean {
return VOLATILE_DIMENSIONS.has(name);
}
/** Check if a dimension name carries semantic content. */
export function isSemanticDimension(name: string): boolean {
return SEMANTIC_DIMENSIONS.has(name);
}
/**
* Compare two values for a dimension, classifying semantic diffs
* from timing/normalization noise.
*
* Volatile dimensions (timing, timestamps, PIDs) are always treated
* as matches because their values are expected to differ across
* runs and do not carry semantic meaning.
*/
export function compareDimension(
dimensionName: string,
expected: unknown,
actual: unknown,
options: { truncated?: boolean } = {},
): DimensionResult {
// Volatile dimensions: always match regardless of values
if (isVolatileDimension(dimensionName)) {
return {
name: dimensionName,
status: "match",
message: "volatile dimension ignored",
evidence_ids: [],
};
}
if (options.truncated) {
return {
name: dimensionName,
status: "truncated",
message: "evidence was truncated",
evidence_ids: [],
};
}
if (expected === undefined && actual === undefined) {
return {
name: dimensionName,
status: "unknown",
message: "both expected and actual are undefined",
evidence_ids: [],
};
}
if (expected === undefined || actual === undefined) {
return {
name: dimensionName,
status: "mismatch",
message: `expected ${expected === undefined ? "present" : "absent"}, actual ${actual === undefined ? "absent" : "present"}`,
evidence_ids: [],
};
}
// Semantic comparison for structured objects
if (typeof expected === "object" && typeof actual === "object") {
const expectedJson = JSON.stringify(sortKeys(expected));
const actualJson = JSON.stringify(sortKeys(actual));
if (expectedJson === actualJson) {
return {
name: dimensionName,
status: "match",
message: "",
evidence_ids: [],
};
}
return {
name: dimensionName,
status: "mismatch",
message: "semantic content differs",
evidence_ids: [],
};
}
// Primitive comparison
if (expected === actual) {
return {
name: dimensionName,
status: "match",
message: "",
evidence_ids: [],
};
}
return {
name: dimensionName,
status: "mismatch",
message: `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
evidence_ids: [],
};
}
/** Recursively sort object keys for deterministic comparison. */
function sortKeys(value: unknown): unknown {
if (value === null || typeof value !== "object") return value;
if (Array.isArray(value)) return value.map(sortKeys);
const sorted: Record<string, unknown> = {};
for (const key of Object.keys(value as Record<string, unknown>).sort()) {
sorted[key] = sortKeys((value as Record<string, unknown>)[key]);
}
return sorted;
}
/**
* Evaluate a trust gate for one scenario by comparing expected
* evidence against the actual captured run.
*/
export function evaluateTrustGate(
contract: VerifierContract,
expectedEvidence: Record<string, unknown>,
actualEvidence: Record<string, unknown>,
options: { truncated?: boolean } = {},
): TrustGateResult {
const dimensionResults: DimensionResult[] = [];
let firstDivergence: TrustGateResult["first_divergence"] = null;
for (const dim of contract.dimensions) {
if (!dim.required) continue;
const expectedValue = expectedEvidence[dim.name];
const actualValue = actualEvidence[dim.name];
const result = compareDimension(
dim.name,
expectedValue,
actualValue,
options,
);
dimensionResults.push(result);
if (result.status === "mismatch" && firstDivergence === null) {
firstDivergence = {
dimension: dim.name,
evidence_id: result.evidence_ids[0] ?? "unknown",
message: result.message,
};
}
}
const hasFail = dimensionResults.some(
(r) => r.status === "mismatch" || r.status === "truncated",
);
const hasUnknown = dimensionResults.some((r) => r.status === "unknown");
const verdict: TrustGateResult["verdict"] = hasFail
? "fail"
: hasUnknown
? "unknown"
: "pass";
return {
scenario_id: contract.scenario_id,
verdict,
dimension_results: dimensionResults,
first_divergence: firstDivergence,
};
}
/**
* Evaluate trust gates for all scenarios in a conformance package.
* Rejects runs that differ in required dimensions. Unknown/truncated
* evidence is never treated as equivalence.
*/
export function evaluatePackageTrustGates(
pkg: ConformancePackage,
actualEvidence: Record<string, Record<string, unknown>>,
options: { truncated?: boolean } = {},
): TrustGateResult[] {
const results: TrustGateResult[] = [];
for (const contract of pkg.verifier_contracts) {
const expected = pkg.expected_evidence.find(
(e) => e.scenario_id === contract.scenario_id,
);
if (!expected) continue;
const expectedEvidence = expected.envelopes[0] ?? {};
const actual = actualEvidence[contract.scenario_id] ?? {};
const result = evaluateTrustGate(
contract,
expectedEvidence as Record<string, unknown>,
actual,
options,
);
results.push(result);
}
return results;
}
+173
View File
@@ -0,0 +1,173 @@
import { z } from "zod";
import { jsonValueSchema } from "./jsonValue.js";
/** Supported transport types for custom protocol capture. */
export const transportTypeSchema = z.enum([
"tcp",
"udp",
"ipc",
"unix-socket",
"named-pipe",
"xpc",
]);
export type TransportType = z.infer<typeof transportTypeSchema>;
/** Direction of a protocol frame. */
export const frameDirectionSchema = z.enum(["sent", "received", "intercepted"]);
export type FrameDirection = z.infer<typeof frameDirectionSchema>;
/** A captured protocol frame. */
export const protocolFrameSchema = z.strictObject({
/** Monotonic sequence number. */
sequence: z.number().int().nonnegative(),
/** Timestamp in milliseconds. */
at_ms: z.number().int().nonnegative(),
/** Transport type. */
transport: transportTypeSchema,
/** Direction of the frame. */
direction: frameDirectionSchema,
/** Source process identity. */
source_pid: z.number().int().nullable(),
/** Destination process identity. */
dest_pid: z.number().int().nullable(),
/** Endpoint address (IP:port, socket path, pipe name). */
source_endpoint: z.string().nullable(),
/** Destination endpoint. */
dest_endpoint: z.string().nullable(),
/** Raw frame data as base64. */
raw_data: z.string().nullable(),
/** Decoded/parsed frame content if known. */
decoded_content: jsonValueSchema.nullable(),
/** Frame size in bytes. */
size: z.number().int().nonnegative(),
/** Whether the frame was truncated. */
truncated: z.boolean().default(false),
});
export type ProtocolFrame = z.infer<typeof protocolFrameSchema>;
/** Authentication flow stage. */
export const authStageSchema = z.enum([
"init",
"challenge",
"response",
"token_exchange",
"renewal",
"revocation",
"failure",
"success",
]);
export type AuthStage = z.infer<typeof authStageSchema>;
/** Token lifecycle event. */
export const tokenLifecycleEventSchema = z.enum([
"issued",
"refreshed",
"expired",
"revoked",
"renewed",
]);
export type TokenLifecycleEvent = z.infer<typeof tokenLifecycleEventSchema>;
/** A captured authentication flow event. */
export const authFlowEventSchema = z.strictObject({
/** Monotonic sequence number. */
sequence: z.number().int().nonnegative(),
/** Timestamp in milliseconds. */
at_ms: z.number().int().nonnegative(),
/** Authentication stage. */
stage: authStageSchema,
/** Protocol used. */
protocol: z.string().min(1),
/** Token type if applicable. */
token_type: z.string().nullable(),
/** Token lifecycle event. */
token_lifecycle: tokenLifecycleEventSchema.nullable(),
/** Correlation ID linking challenge-response pairs. */
correlation_id: z.string().nullable(),
/** Whether credentials were detected and redacted. */
credentials_redacted: z.boolean().default(false),
/** Whether the authentication succeeded. */
succeeded: z.boolean().default(false),
/** Error message if authentication failed. */
error: z.string().nullable(),
});
export type AuthFlowEvent = z.infer<typeof authFlowEventSchema>;
/** A captured protocol session. */
export const customProtocolCaptureSchema = z.strictObject({
/** Transport type for this session. */
transport: transportTypeSchema,
/** Captured frames in order. */
frames: z.array(protocolFrameSchema).min(0).max(100_000),
/** Authentication flow events. */
auth_events: z.array(authFlowEventSchema).default([]),
/** Whether any frame was truncated. */
has_truncated: z.boolean().default(false),
/** Whether credentials were detected. */
credentials_detected: z.boolean().default(false),
});
export type CustomProtocolCapture = z.infer<typeof customProtocolCaptureSchema>;
/** Correlate a frame with process identity. */
export function correlateProcessIdentity(
frame: ProtocolFrame,
pid: number,
): boolean {
return frame.source_pid === pid || frame.dest_pid === pid;
}
/** Check if a frame contains credential-like content. */
export function looksLikeCredentialFrame(frame: ProtocolFrame): boolean {
const patterns = [
"password",
"token",
"secret",
"api_key",
"apikey",
"authorization",
"credential",
];
const content = JSON.stringify(frame.decoded_content ?? "").toLowerCase();
return patterns.some((p) => content.includes(p));
}
/** Filter frames by transport type. */
export function framesByTransport(
frames: readonly ProtocolFrame[],
transport: TransportType,
): ProtocolFrame[] {
return frames.filter((f) => f.transport === transport);
}
/** Get all authentication flow events for a specific protocol. */
export function authEventsByProtocol(
events: readonly AuthFlowEvent[],
protocol: string,
): AuthFlowEvent[] {
return events.filter((e) => e.protocol === protocol);
}
/** Get successful authentication events. */
export function successfulAuthEvents(
events: readonly AuthFlowEvent[],
): AuthFlowEvent[] {
return events.filter((e) => e.succeeded);
}
/** Get failed authentication events. */
export function failedAuthEvents(
events: readonly AuthFlowEvent[],
): AuthFlowEvent[] {
return events.filter((e) => !e.succeeded);
}
/** Compute the duration of an authentication flow in milliseconds. */
export function authFlowDuration(
events: readonly AuthFlowEvent[],
): number | null {
if (events.length === 0) return null;
const first = events[0]!;
const last = events[events.length - 1]!;
return last.at_ms - first.at_ms;
}
+240
View File
@@ -0,0 +1,240 @@
import { z } from "zod";
/** Event types for event-backed process-tree capture. */
export const processEventTypeSchema = z.enum([
"spawn",
"exit",
"re_exec",
"reparent",
"signal",
"name_change",
"thread_create",
"thread_exit",
]);
export type ProcessEventType = z.infer<typeof processEventTypeSchema>;
/** A single event-backed process-tree event. */
export const processTreeEventSchema = z.strictObject({
/** Monotonic event sequence number. */
sequence: z.number().int().nonnegative(),
/** Event timestamp in milliseconds. */
at_ms: z.number().int().nonnegative(),
/** Event type. */
type: processEventTypeSchema,
/** Process ID. */
pid: z.number().int().positive(),
/** Parent process ID. */
ppid: z.number().int().nullable(),
/** Process name if known. */
process_name: z.string().nullable(),
/** Executable path if known. */
executable: z.string().nullable(),
/** Command line arguments. */
arguments: z.array(z.string()).default([]),
/** Previous PID (for reparent events). */
previous_pid: z.number().int().nullable(),
/** Previous PPID (for reparent events). */
previous_ppid: z.number().int().nullable(),
/** Exit code (for exit events). */
exit_code: z.number().int().nullable(),
/** Signal number (for signal events). */
signal: z.number().int().nullable(),
/** Thread ID (for thread events). */
tid: z.number().int().nullable(),
});
export type ProcessTreeEvent = z.infer<typeof processTreeEventSchema>;
/** Process node in a reconstructed tree. */
export const processNodeSchema = z.strictObject({
pid: z.number().int().positive(),
ppid: z.number().int().nullable(),
process_name: z.string().nullable(),
executable: z.string().nullable(),
arguments: z.array(z.string()).default([]),
children: z.array(z.number().int()).default([]),
spawn_time_ms: z.number().int().nullable(),
exit_time_ms: z.number().int().nullable(),
exit_code: z.number().int().nullable(),
is_re_exec: z.boolean().default(false),
is_reparented: z.boolean().default(false),
previous_ppid: z.number().int().nullable(),
});
export type ProcessNode = z.infer<typeof processNodeSchema>;
/** Result of event-backed process-tree reconstruction. */
export const processTreeReconstructionSchema = z.strictObject({
/** Root PID of the process tree. */
root_pid: z.number().int().positive(),
/** All reconstructed process nodes. */
processes: z.array(processNodeSchema).min(0).max(10_000),
/** Events that were consumed. */
events_consumed: z.number().int().nonnegative(),
/** Events that could not be matched. */
events_unmatched: z.number().int().nonnegative(),
/** Short-lived descendants that were missed by sampling. */
short_lived_descendants: z.number().int().nonnegative(),
/** Whether any re-exec was detected. */
has_re_exec: z.boolean().default(false),
/** Whether any reparenting was detected. */
has_reparenting: z.boolean().default(false),
});
export type ProcessTreeReconstruction = z.infer<
typeof processTreeReconstructionSchema
>;
/** Reconstruct a process tree from event-backed capture. */
export function reconstructProcessTree(
events: readonly ProcessTreeEvent[],
rootPid: number,
): ProcessTreeReconstruction {
const nodes = new Map<number, ProcessNode>();
const children = new Map<number, number[]>();
let eventsConsumed = 0;
let eventsUnmatched = 0;
let hasReExec = false;
let hasReparenting = false;
let shortLivedDescendants = 0;
const sorted = [...events].sort((a, b) => a.at_ms - b.at_ms);
for (const event of sorted) {
eventsConsumed++;
let node = nodes.get(event.pid);
switch (event.type) {
case "spawn": {
if (!node) {
node = {
pid: event.pid,
ppid: event.ppid,
process_name: event.process_name,
executable: event.executable,
arguments: event.arguments,
children: [],
spawn_time_ms: event.at_ms,
exit_time_ms: null,
exit_code: null,
is_re_exec: false,
is_reparented: false,
previous_ppid: null,
};
nodes.set(event.pid, node);
}
if (event.ppid !== null) {
const parentChildren = children.get(event.ppid) ?? [];
parentChildren.push(event.pid);
children.set(event.ppid, parentChildren);
}
break;
}
case "exit": {
if (node) {
node.exit_time_ms = event.at_ms;
node.exit_code = event.exit_code;
if (
node.spawn_time_ms !== null &&
event.at_ms - node.spawn_time_ms < 100
)
shortLivedDescendants++;
} else {
eventsUnmatched++;
}
break;
}
case "re_exec": {
if (node) {
node.is_re_exec = true;
node.executable = event.executable ?? node.executable;
node.arguments = event.arguments;
hasReExec = true;
} else {
eventsUnmatched++;
}
break;
}
case "reparent": {
if (node) {
node.previous_ppid = node.ppid;
node.ppid = event.ppid;
node.is_reparented = true;
hasReparenting = true;
} else {
eventsUnmatched++;
}
break;
}
case "signal":
case "name_change":
case "thread_create":
case "thread_exit":
// These events are recorded but don't modify the tree structure
break;
}
}
// Attach children to nodes
for (const [pid, childPids] of children) {
const node = nodes.get(pid);
if (node) node.children = childPids;
}
return {
root_pid: rootPid,
processes: [...nodes.values()],
events_consumed: eventsConsumed,
events_unmatched: eventsUnmatched,
short_lived_descendants: shortLivedDescendants,
has_re_exec: hasReExec,
has_reparenting: hasReparenting,
};
}
/** Get all descendant PIDs of a given root PID. */
export function getDescendants(
processes: readonly ProcessNode[],
rootPid: number,
): number[] {
const byPid = new Map(processes.map((p) => [p.pid, p]));
const visited = new Set<number>();
const result: number[] = [];
const queue: number[] = [rootPid];
while (queue.length > 0) {
const pid = queue.shift()!;
if (visited.has(pid)) continue;
visited.add(pid);
const node = byPid.get(pid);
if (node) {
for (const child of node.children) {
if (!visited.has(child)) {
result.push(child);
queue.push(child);
}
}
}
}
return result;
}
/** Check if a process tree contains a specific PID. */
export function containsProcess(
processes: readonly ProcessNode[],
pid: number,
): boolean {
return processes.some((p) => p.pid === pid);
}
/** Get the maximum tree depth from the root. */
export function maxTreeDepth(
processes: readonly ProcessNode[],
rootPid: number,
): number {
const byPid = new Map(processes.map((p) => [p.pid, p]));
function depth(pid: number, visited: Set<number>): number {
if (visited.has(pid)) return 0;
visited.add(pid);
const node = byPid.get(pid);
if (!node || node.children.length === 0) return 0;
return 1 + Math.max(...node.children.map((c) => depth(c, visited)));
}
return depth(rootPid, new Set());
}
+73 -1
View File
@@ -239,7 +239,7 @@ const validateUnknownGraph = (
const validateVerifiedResolution = (
unknown: ResidualUnknown,
evidenceById: ReadonlyMap<string, Evidence>,
evidenceById: Pick<ReadonlyMap<string, Evidence>, "get">,
): void => {
if (unknown.contradicting_evidence_ids.length > 0)
throw new TypeError(
@@ -260,6 +260,78 @@ const validateVerifiedResolution = (
);
};
/**
* Validate one new residual-unknown revision against an already valid ledger.
* This keeps ordinary mutations proportional to the affected revision while
* full bundle imports continue to validate the complete graph.
*/
export const validateResidualUnknownAddition = (
unknown: ResidualUnknown,
evidenceById: Pick<ReadonlyMap<string, Evidence>, "get" | "has">,
currentHeads: ReadonlyMap<string, ResidualUnknown>,
): void => {
const current = currentHeads.get(unknown.unknown_id);
if (
current === undefined
? unknown.revision !== 1 || unknown.previous_revision_digest !== null
: unknown.revision !== current.revision + 1 ||
unknown.previous_revision_digest !== current.revision_digest
)
throw new TypeError(
`Residual unknown ${unknown.unknown_id} revision chain is broken`,
);
for (const evidenceId of referencedEvidenceIds(unknown))
if (!evidenceById.has(evidenceId))
throw new TypeError(
`Residual unknown references missing evidence ${evidenceId}`,
);
for (const relationship of unknown.relationships)
if (
relationship.unknown_id !== unknown.unknown_id &&
!currentHeads.has(relationship.unknown_id)
)
throw new TypeError(
`Residual unknown references missing unknown ${relationship.unknown_id}`,
);
if (unknown.resolution?.disposition === "verified")
validateVerifiedResolution(unknown, evidenceById);
for (const relationship of unknown.relationships)
if (
relationship.type === "depends-on" &&
reachesUnknown(
relationship.unknown_id,
unknown.unknown_id,
currentHeads,
unknown,
)
)
throw new TypeError("Residual unknown dependency graph contains a cycle");
};
const reachesUnknown = (
start: string,
goal: string,
currentHeads: ReadonlyMap<string, ResidualUnknown>,
candidate: ResidualUnknown,
): boolean => {
const pending = [start];
const visited = new Set<string>();
while (pending.length > 0) {
const id = pending.pop();
if (id === undefined) break;
if (id === goal) return true;
if (visited.has(id)) continue;
visited.add(id);
const unknown =
id === candidate.unknown_id ? candidate : currentHeads.get(id);
if (unknown === undefined) continue;
for (const relationship of unknown.relationships)
if (relationship.type === "depends-on")
pending.push(relationship.unknown_id);
}
return false;
};
const evidenceQualifies = (
unknown: ResidualUnknown,
evidence: Evidence,
+1 -1
View File
@@ -65,7 +65,7 @@ const failures = [
[
75,
"hopper_exited_during_startup",
"Hopper exited before the REA bridge was ready. Run rea doctor and retry the analysis.",
"Hopper exited before the REA bridge was ready. In CI, run rea doctor --provider hopper --detail full --json; if Hopper showed a UI or license prompt, complete it in the runner user session, otherwise close stale Hopper sessions and retry.",
],
[
76,
+4 -2
View File
@@ -1,8 +1,9 @@
import { z } from "zod";
import type { JsonValue } from "./jsonValue.js";
import { jsonValueSchema, type JsonValue } from "./jsonValue.js";
import { AnalysisOutputError, HopperProtocolError } from "./errors.js";
import { err, ok, type Result } from "./result.js";
import { nativeApiBoundarySchema } from "./nativeApiBoundary.js";
export interface AddressedName {
readonly address: string;
@@ -275,6 +276,7 @@ export const functionDossierSchema = z
instruction_scan: z
.object({ scanned: z.number().int().min(0), truncated: z.boolean() })
.strict(),
native_api: nativeApiBoundarySchema.optional(),
limitations: z.array(z.string()).default([]),
})
.strict();
@@ -288,7 +290,7 @@ export const parseFunctionDossier = (
): Result<JsonValue, AnalysisOutputError> => {
const parsed = functionDossierSchema.safeParse(value);
return parsed.success
? ok(parsed.data)
? ok(jsonValueSchema.parse(parsed.data))
: err(
new AnalysisOutputError(
"analyze_function",
+15 -11
View File
@@ -1,4 +1,3 @@
import { parse } from "@babel/parser";
import * as t from "@babel/types";
import {
@@ -41,6 +40,10 @@ import {
propertyName,
range,
} from "./javascriptStaticAnalysisHelpers.js";
import {
parseJavaScriptSource,
type ParsedJavaScriptSource,
} from "./javascriptSourceParser.js";
interface BindPatternInput {
readonly pattern: t.Node;
@@ -68,18 +71,19 @@ interface AddBindingInput {
export const analyzeJavaScriptSemantics = (
source: string,
inputLimits: Partial<JavaScriptSemanticLimits> = {},
): JavaScriptSemanticIr => {
const file = parseJavaScriptSource(source);
return file === null
? failedJavaScriptSemanticIr()
: analyzeParsedJavaScriptSemantics(file, inputLimits);
};
/** Recover semantics from an already parsed JavaScript artifact. */
export const analyzeParsedJavaScriptSemantics = (
file: ParsedJavaScriptSource,
inputLimits: Partial<JavaScriptSemanticLimits> = {},
): JavaScriptSemanticIr => {
const limits = { ...DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS, ...inputLimits };
let file: ReturnType<typeof parse>;
try {
file = parse(source, {
sourceType: "unambiguous",
errorRecovery: true,
plugins: ["jsx", "typescript"],
});
} catch {
return failedJavaScriptSemanticIr();
}
const state = createState(file.program, limits);
collectDefinitions(file.program, state);
const references = collectSemanticReferences(file.program, state);
+20
View File
@@ -0,0 +1,20 @@
import { parse } from "@babel/parser";
/** Babel AST produced by REA's inert JavaScript parser boundary. */
export type ParsedJavaScriptSource = ReturnType<typeof parse>;
/** Parse JavaScript or TypeScript once without retaining unused comment nodes. */
export const parseJavaScriptSource = (
source: string,
): ParsedJavaScriptSource | null => {
try {
return parse(source, {
sourceType: "unambiguous",
errorRecovery: true,
attachComment: false,
plugins: ["jsx", "typescript"],
});
} catch {
return null;
}
};
+18 -13
View File
@@ -1,4 +1,3 @@
import { parse } from "@babel/parser";
import * as t from "@babel/types";
import { inspectElectronStaticNode } from "./electronStaticAnalysis.js";
@@ -29,22 +28,28 @@ import type {
JavaScriptStaticAnalysis,
JavaScriptStaticAnalysisLimits,
} from "./javascriptStaticAnalysisTypes.js";
import {
parseJavaScriptSource,
type ParsedJavaScriptSource,
} from "./javascriptSourceParser.js";
/** Parse one bounded JavaScript artifact and recover static structure only. */
export const analyzeJavaScriptStaticSource = (
source: string,
limits: JavaScriptStaticAnalysisLimits,
): JavaScriptStaticAnalysis => {
let file: ReturnType<typeof parse>;
try {
file = parse(source, {
sourceType: "unambiguous",
errorRecovery: true,
plugins: ["jsx", "typescript"],
});
} catch {
return failedJavaScriptStaticAnalysis();
}
const file = parseJavaScriptSource(source);
return file === null
? failedJavaScriptStaticAnalysis()
: analyzeParsedJavaScriptStaticSource(source, file, limits);
};
/** Recover static structure from an already parsed JavaScript artifact. */
export const analyzeParsedJavaScriptStaticSource = (
source: string,
file: ParsedJavaScriptSource,
limits: JavaScriptStaticAnalysisLimits,
): JavaScriptStaticAnalysis => {
const accumulator = createJavaScriptAnalysisAccumulator();
traverseStaticSource(source, file, accumulator, limits);
addSourceMapDirectives(source, accumulator, limits.maxFindings);
@@ -53,7 +58,7 @@ export const analyzeJavaScriptStaticSource = (
const traverseStaticSource = (
source: string,
file: ReturnType<typeof parse>,
file: ParsedJavaScriptSource,
accumulator: AnalysisAccumulator,
limits: JavaScriptStaticAnalysisLimits,
): void => {
@@ -76,7 +81,7 @@ const traverseStaticSource = (
const finalizeStaticAnalysis = (
source: string,
file: ReturnType<typeof parse>,
file: ParsedJavaScriptSource,
accumulator: AnalysisAccumulator,
limits: JavaScriptStaticAnalysisLimits,
): JavaScriptStaticAnalysis => {
@@ -0,0 +1,179 @@
import { z } from "zod";
/** Supported mobile artifact formats. */
export const mobileArtifactFormatSchema = z.enum([
"apk",
"aab",
"ipa",
"dex",
"native_lib",
]);
export type MobileArtifactFormat = z.infer<typeof mobileArtifactFormatSchema>;
/** Mobile platform. */
export const mobilePlatformSchema = z.enum([
"android",
"ios",
"cross_platform",
]);
export type MobilePlatform = z.infer<typeof mobilePlatformSchema>;
/** Android manifest entry. */
export const androidManifestSchema = z.strictObject({
package_name: z.string().min(1),
version_name: z.string().nullable(),
version_code: z.number().int().nullable(),
min_sdk: z.number().int().nullable(),
target_sdk: z.number().int().nullable(),
permissions: z.array(z.string()).default([]),
activities: z.array(z.string()).default([]),
services: z.array(z.string()).default([]),
receivers: z.array(z.string()).default([]),
providers: z.array(z.string()).default([]),
});
export type AndroidManifest = z.infer<typeof androidManifestSchema>;
/** iOS app manifest (Info.plist). */
export const iosManifestSchema = z.strictObject({
bundle_identifier: z.string().min(1),
bundle_version: z.string().nullable(),
short_version: z.string().nullable(),
minimum_os_version: z.string().nullable(),
supported_platforms: z.array(z.string()).default([]),
ui_required_device_capabilities: z.array(z.string()).default([]),
app_permissions: z.array(z.string()).default([]),
});
export type IosManifest = z.infer<typeof iosManifestSchema>;
/** Signing information. */
export const signingInfoSchema = z.strictObject({
is_signed: z.boolean().default(false),
signer_identity: z.string().nullable(),
certificate_fingerprint: z.string().nullable(),
signature_scheme: z.string().nullable(),
is_debuggable: z.boolean().default(false),
});
export type SigningInfo = z.infer<typeof signingInfoSchema>;
/** Native library entry. */
export const nativeLibrarySchema = z.strictObject({
path: z.string().min(1),
architecture: z.string().nullable(),
size: z.number().int().nonnegative(),
digest: z
.string()
.regex(/^[a-f0-9]{64}$/u)
.nullable(),
});
export type NativeLibrary = z.infer<typeof nativeLibrarySchema>;
/** Resource entry within a mobile artifact. */
export const mobileResourceSchema = z.strictObject({
path: z.string().min(1),
type: z.enum([
"layout",
"drawable",
"string",
"asset",
"raw",
"native_lib",
"manifest",
"entitlements",
"provisioning",
"other",
]),
size: z.number().int().nonnegative(),
});
export type MobileResource = z.infer<typeof mobileResourceSchema>;
/** Static analysis result for a mobile artifact. */
export const mobileArtifactManifestSchema = z.strictObject({
format: mobileArtifactFormatSchema,
platform: mobilePlatformSchema,
app_name: z.string().nullable(),
android_manifest: androidManifestSchema.nullable(),
ios_manifest: iosManifestSchema.nullable(),
signing: signingInfoSchema,
resources: z.array(mobileResourceSchema).default([]),
native_libraries: z.array(nativeLibrarySchema).default([]),
total_size: z.number().int().nonnegative(),
architectures: z.array(z.string()).default([]),
});
export type MobileArtifactManifest = z.infer<
typeof mobileArtifactManifestSchema
>;
/** Detect mobile artifact format from file extension. */
export function detectMobileFormat(path: string): MobileArtifactFormat | null {
const lower = path.toLowerCase();
if (lower.endsWith(".apk")) return "apk";
if (lower.endsWith(".aab")) return "aab";
if (lower.endsWith(".ipa")) return "ipa";
if (lower.endsWith(".dex")) return "dex";
if (lower.endsWith(".so") || lower.endsWith(".dylib") || lower.endsWith(".a"))
return "native_lib";
return null;
}
/** Detect platform from format. */
export function detectPlatform(format: MobileArtifactFormat): MobilePlatform {
switch (format) {
case "apk":
case "aab":
case "dex":
return "android";
case "ipa":
return "ios";
case "native_lib":
return "cross_platform";
}
}
/** Classify a resource by its path. */
export function classifyMobileResource(path: string): MobileResource["type"] {
const lower = path.toLowerCase();
if (lower.includes("androidmanifest.xml")) return "manifest";
if (lower.includes(".entitlements")) return "entitlements";
if (lower.includes("provisioning") || lower.includes(".mobileprovision"))
return "provisioning";
if (lower.includes("/layout/")) return "layout";
if (lower.includes("/drawable/") || lower.includes("/mipmap/"))
return "drawable";
if (lower.includes("/values/")) return "string";
if (lower.includes("assets/") || lower.includes("raw/")) return "asset";
if (lower.endsWith(".so") || lower.endsWith(".dylib")) return "native_lib";
if (lower.endsWith(".xml")) return "layout";
return "other";
}
/** Get all native libraries for a specific architecture. */
export function nativeLibsByArchitecture(
manifest: MobileArtifactManifest,
arch: string,
): NativeLibrary[] {
return manifest.native_libraries.filter((l) => l.architecture === arch);
}
/** Check if an Android artifact has internet permission. */
export function hasInternetPermission(
manifest: MobileArtifactManifest,
): boolean {
if (!manifest.android_manifest) return false;
return manifest.android_manifest.permissions.some(
(p) => p.includes("INTERNET") || p.includes("android.permission.INTERNET"),
);
}
/** Get all unique architectures across native libraries. */
export function getAllArchitectures(
manifest: MobileArtifactManifest,
): string[] {
const archs = new Set<string>();
for (const lib of manifest.native_libraries) {
if (lib.architecture) archs.add(lib.architecture);
}
for (const arch of manifest.architectures) {
archs.add(arch);
}
return [...archs];
}
+153
View File
@@ -0,0 +1,153 @@
import { z } from "zod";
const inferenceConfidenceSchema = z.enum(["low", "medium", "high"]);
const inferenceEvidenceSchema = z
.object({
kind: z.enum([
"signature-source",
"decompiler-type",
"type-lock",
"calling-convention",
"jump-table",
]),
source: z.string().min(1),
detail: z.string().min(1),
})
.strict();
const inferredBoundaryTypeSchema = z
.object({
role: z.enum(["return", "parameter"]),
ordinal: z.number().int().min(0).nullable(),
name: z.string().min(1).nullable(),
data_type: z.string().min(1),
size_bytes: z.number().int().min(0).nullable(),
storage: z.string().min(1).nullable(),
confidence: inferenceConfidenceSchema,
evidence: z.array(inferenceEvidenceSchema).min(1),
decompiler_artifacts: z.array(
z.enum([
"recovered-signature",
"register-or-stack-storage",
"compiler-generated-variable",
]),
),
})
.strict();
const jumpTableDataSourceSchema = z
.object({
address: z.string().min(1),
provenance: z.enum([
"ghidra-decompiler-load-table",
"ghidra-dispatch-block-data-reference",
]),
entry_size_bytes: z.number().int().min(1).nullable(),
entry_count: z.number().int().min(0).nullable(),
confidence: inferenceConfidenceSchema,
evidence: z.array(inferenceEvidenceSchema).min(1),
})
.strict();
const jumpTableMappingSchema = z
.object({
case_value: z.number().int().nullable(),
target_address: z.string().min(1),
data_addresses: z.array(z.string().min(1)),
confidence: inferenceConfidenceSchema,
evidence: z.array(inferenceEvidenceSchema).min(1),
})
.strict();
const inferredJumpTableSchema = z
.object({
dispatch_address: z.string().min(1),
data_sources: z.array(jumpTableDataSourceSchema),
data_sources_truncated: z.boolean(),
mappings: z.array(jumpTableMappingSchema),
mappings_truncated: z.boolean(),
limitations: z.array(z.string()),
})
.strict();
const availableNativeApiBoundarySchema = z
.object({
available: z.literal(true),
provenance: z.string().min(1),
signature_source: z.string().min(1),
calling_convention: z.string().min(1),
return_type: inferredBoundaryTypeSchema,
parameters: z.array(inferredBoundaryTypeSchema),
parameters_truncated: z.boolean(),
jump_tables: z.array(inferredJumpTableSchema),
jump_tables_truncated: z.boolean(),
pseudocode: z
.object({
classification: z.literal("decompiler-generated-non-source"),
compilable: z.literal(false),
})
.strict(),
decompiler_artifacts: z.array(
z.enum([
"recovered-signature",
"register-and-stack-variables",
"compiler-generated-control-flow",
"pointer-arithmetic",
"pseudocode",
]),
),
limitations: z.array(z.string()),
})
.strict();
const unavailableNativeApiBoundarySchema = z
.object({
available: z.literal(false),
reason: z.string().min(1),
residual_unknowns: z.array(z.string().min(1)).min(1),
})
.strict();
/** Provider-neutral native API boundary observations attached to a function dossier. */
export const nativeApiBoundarySchema = z.discriminatedUnion("available", [
unavailableNativeApiBoundarySchema,
availableNativeApiBoundarySchema,
]);
/** One validated provider-neutral native API boundary observation. */
export type NativeApiBoundary = z.infer<typeof nativeApiBoundarySchema>;
const nativeApiInspectionSubstepSchema = z
.object({
operation: z.enum([
"analyze_function",
"project_native_api_boundary",
"preserve_residual_unknowns",
]),
status: z.enum(["completed", "unsupported"]),
observations: z.array(z.string().min(1)),
})
.strict();
/** Agent-facing projection of structured native API reconstruction substeps. */
export const nativeApiInspectionResultSchema = z
.object({
schema_version: z.literal(1),
procedure: z
.object({
address: z.string().min(1),
name: z.string().min(1),
})
.strict(),
boundary: nativeApiBoundarySchema,
substeps: z.array(nativeApiInspectionSubstepSchema).min(3).max(3),
unsupported_branches: z.array(z.string().min(1)),
residual_unknowns: z.array(z.string().min(1)),
})
.strict();
/** One validated agent-facing native API inspection result. */
export type NativeApiInspectionResult = z.infer<
typeof nativeApiInspectionResultSchema
>;
+192
View File
@@ -0,0 +1,192 @@
import { z } from "zod";
/** Objective-C method type. */
export const objcMethodTypeSchema = z.enum([
"instance",
"class",
"ivar_getter",
"ivar_setter",
]);
export type ObjcMethodType = z.infer<typeof objcMethodTypeSchema>;
/** Objective-C property attribute. */
export const objcPropertyAttributeSchema = z.strictObject({
name: z.string().min(1),
value: z.string(),
is_weak: z.boolean().default(false),
is_atomic: z.boolean().default(false),
is_copy: z.boolean().default(false),
is_strong: z.boolean().default(false),
});
export type ObjcPropertyAttribute = z.infer<typeof objcPropertyAttributeSchema>;
/** Objective-C method metadata. */
export const objcMethodSchema = z.strictObject({
selector: z.string().min(1),
method_type: objcMethodTypeSchema,
address: z.number().int().nullable(),
is_required: z.boolean().default(false),
is_optional: z.boolean().default(false),
});
export type ObjcMethod = z.infer<typeof objcMethodSchema>;
/** Objective-C property metadata. */
export const objcPropertySchema = z.strictObject({
name: z.string().min(1),
type_encoding: z.string().nullable(),
attributes: z.array(objcPropertyAttributeSchema).default([]),
is_readonly: z.boolean().default(false),
getter: z.string().nullable(),
setter: z.string().nullable(),
});
export type ObjcProperty = z.infer<typeof objcPropertySchema>;
/** Objective-C protocol metadata. */
export const objcProtocolSchema = z.strictObject({
name: z.string().min(1),
methods: z.array(objcMethodSchema).default([]),
optional_methods: z.array(objcMethodSchema).default([]),
properties: z.array(objcPropertySchema).default([]),
});
export type ObjcProtocol = z.infer<typeof objcProtocolSchema>;
/** Objective-C class metadata. */
export const objcClassSchema = z.strictObject({
name: z.string().min(1),
super_class: z.string().nullable(),
is_meta_class: z.boolean().default(false),
is_root_class: z.boolean().default(false),
methods: z.array(objcMethodSchema).default([]),
properties: z.array(objcPropertySchema).default([]),
protocols: z.array(z.string()).default([]),
ivar_count: z.number().int().nonnegative().default(0),
instance_size: z.number().int().nonnegative().nullable(),
});
export type ObjcClass = z.infer<typeof objcClassSchema>;
/** Swift declaration kind. */
export const swiftDeclKindSchema = z.enum([
"class",
"struct",
"enum",
"protocol",
"extension",
"actor",
"global_var",
"global_func",
]);
export type SwiftDeclKind = z.infer<typeof swiftDeclKindSchema>;
/** Swift access level. */
export const swiftAccessLevelSchema = z.enum([
"private",
"fileprivate",
"internal",
"public",
"open",
]);
export type SwiftAccessLevel = z.infer<typeof swiftAccessLevelSchema>;
/** Swift declaration metadata. */
export const swiftDeclSchema = z.strictObject({
kind: swiftDeclKindSchema,
name: z.string().min(1),
module: z.string().nullable(),
access_level: swiftAccessLevelSchema.default("internal"),
super_class: z.string().nullable(),
protocols: z.array(z.string()).default([]),
is_final: z.boolean().default(false),
is_required: z.boolean().default(false),
is_convenience_init: z.boolean().default(false),
is_override: z.boolean().default(false),
});
export type SwiftDecl = z.infer<typeof swiftDeclSchema>;
/** Database save/read operation type. */
export const dbOperationSchema = z.enum([
"save",
"readback",
"open_database",
"close_database",
"export",
"import",
]);
export type DbOperation = z.infer<typeof dbOperationSchema>;
/** Database save operation result. */
export const dbSaveResultSchema = z.strictObject({
operation: dbOperationSchema,
succeeded: z.boolean(),
preserved_names: z.number().int().nonnegative().default(0),
preserved_comments: z.number().int().nonnegative().default(0),
preserved_bookmarks: z.number().int().nonnegative().default(0),
database_path: z.string().nullable(),
error: z.string().nullable(),
});
export type DbSaveResult = z.infer<typeof dbSaveResultSchema>;
/** Result of deeper ObjC/Swift metadata extraction. */
export const objcSwiftMetadataSchema = z.strictObject({
objc_classes: z.array(objcClassSchema).default([]),
objc_protocols: z.array(objcProtocolSchema).default([]),
swift_decls: z.array(swiftDeclSchema).default([]),
db_save_result: dbSaveResultSchema.nullable(),
});
export type ObjcSwiftMetadata = z.infer<typeof objcSwiftMetadataSchema>;
/** Check if a selector name looks like a getter. */
export function isGetterSelector(selector: string): boolean {
return /^[a-z][a-zA-Z0-9_]*$/u.test(selector) && !selector.includes(":");
}
/** Check if a selector name looks like a setter. */
export function isSetterSelector(selector: string): boolean {
return selector.startsWith("set") && selector.endsWith(":");
}
/** Extract property name from a getter/setter selector. */
export function propertyNameFromSelector(selector: string): string | null {
if (isSetterSelector(selector)) {
const inner = selector.slice(3, -1);
return inner.charAt(0).toLowerCase() + inner.slice(1);
}
if (isGetterSelector(selector)) {
return selector;
}
return null;
}
/** Count ObjC methods by type. */
export function countMethodsByType(
methods: readonly ObjcMethod[],
): Record<ObjcMethodType, number> {
return {
instance: methods.filter((m) => m.method_type === "instance").length,
class: methods.filter((m) => m.method_type === "class").length,
ivar_getter: methods.filter((m) => m.method_type === "ivar_getter").length,
ivar_setter: methods.filter((m) => m.method_type === "ivar_setter").length,
};
}
/** Get all Swift declarations of a specific kind. */
export function swiftDeclsByKind(
decls: readonly SwiftDecl[],
kind: SwiftDeclKind,
): SwiftDecl[] {
return decls.filter((d) => d.kind === kind);
}
/** Check if a database save preserved all expected items. */
export function isDbSaveComplete(
result: DbSaveResult,
expectedNames: number,
expectedComments: number,
expectedBookmarks: number,
): boolean {
return (
result.succeeded &&
result.preserved_names >= expectedNames &&
result.preserved_comments >= expectedComments &&
result.preserved_bookmarks >= expectedBookmarks
);
}
+144
View File
@@ -0,0 +1,144 @@
import { z } from "zod";
/** Supported Windows package formats. */
export const packageFormatSchema = z.enum(["msix", "msi", "appx"]);
export type PackageFormat = z.infer<typeof packageFormatSchema>;
/** A resource entry within a package. */
export const packageResourceSchema = z.strictObject({
/** Resource path within the package. */
path: z.string().min(1),
/** Resource type. */
type: z.enum([
"manifest",
"icon",
"configuration",
"certificate",
"executable",
"library",
"data",
"other",
]),
/** Size in bytes. */
size: z.number().int().nonnegative(),
/** SHA-256 digest. */
digest: z
.string()
.regex(/^[a-f0-9]{64}$/u)
.nullable(),
});
export type PackageResource = z.infer<typeof packageResourceSchema>;
/** Digital signature information. */
export const digitalSignatureSchema = z.strictObject({
/** Whether the package is signed. */
is_signed: z.boolean(),
/** Signer subject name. */
signer_subject: z.string().nullable(),
/** Certificate thumbprint. */
certificate_thumbprint: z.string().nullable(),
/** Whether the signature is valid. */
is_valid: z.boolean().default(false),
/** Whether the signature is timestamped. */
is_timestamped: z.boolean().default(false),
/** Timestamp signer if present. */
timestamp_signer: z.string().nullable(),
});
export type DigitalSignature = z.infer<typeof digitalSignatureSchema>;
/** A Windows package manifest for static analysis. */
export const packageManifestSchema = z.strictObject({
/** Package format. */
format: packageFormatSchema,
/** Package identity name. */
name: z.string().min(1),
/** Package publisher. */
publisher: z.string().nullable(),
/** Package version. */
version: z.string().nullable(),
/** Architecture. */
architecture: z.enum(["x86", "x64", "arm", "arm64", "neutral", "unknown"]),
/** Package resources. */
resources: z.array(packageResourceSchema).default([]),
/** Digital signature. */
signature: digitalSignatureSchema,
/** Whether the package is a framework. */
is_framework: z.boolean().default(false),
/** Package dependencies. */
dependencies: z.array(z.string()).default([]),
/** Capabilities declared in the manifest. */
capabilities: z.array(z.string()).default([]),
});
export type PackageManifest = z.infer<typeof packageManifestSchema>;
/** Detect package format from file extension. */
export function detectPackageFormat(path: string): PackageFormat | null {
const lower = path.toLowerCase();
if (lower.endsWith(".msix")) return "msix";
if (lower.endsWith(".msi")) return "msi";
if (lower.endsWith(".appx")) return "appx";
return null;
}
/** Check if a resource path looks like a manifest. */
export function isManifestResource(path: string): boolean {
const lower = path.toLowerCase();
return (
lower.includes("appxmanifest.xml") ||
lower.includes("appxmanifest") ||
lower.includes("manifest.xml") ||
lower.endsWith("_manifest.xml")
);
}
/** Check if a resource path looks like a configuration file. */
export function isConfigurationResource(path: string): boolean {
const lower = path.toLowerCase();
return (
lower.endsWith(".xml") ||
lower.endsWith(".json") ||
lower.endsWith(".ini") ||
lower.endsWith(".config")
);
}
/** Classify a resource by its path. */
export function classifyResource(path: string): PackageResource["type"] {
if (isManifestResource(path)) return "manifest";
const lower = path.toLowerCase();
if (
lower.endsWith(".ico") ||
lower.endsWith(".png") ||
lower.endsWith(".jpg") ||
lower.endsWith(".jpeg")
)
return "icon";
if (isConfigurationResource(path)) return "configuration";
if (
lower.endsWith(".cer") ||
lower.endsWith(".pfx") ||
lower.endsWith(".p12")
)
return "certificate";
if (lower.endsWith(".exe") || lower.endsWith(".dll"))
return lower.endsWith(".exe") ? "executable" : "library";
return "other";
}
/** Get all resources of a specific type. */
export function resourcesByType(
manifest: PackageManifest,
type: PackageResource["type"],
): PackageResource[] {
return manifest.resources.filter((r) => r.type === type);
}
/** Get the total size of all resources. */
export function totalResourceSize(manifest: PackageManifest): number {
return manifest.resources.reduce((sum, r) => sum + r.size, 0);
}
/** Check if a package has a valid signature. */
export function hasValidSignature(manifest: PackageManifest): boolean {
return manifest.signature.is_signed && manifest.signature.is_valid;
}
+225
View File
@@ -0,0 +1,225 @@
import { z } from "zod";
/** PE/COFF section characteristics flags. */
export const sectionCharacteristicsSchema = z.object({
/** Section contains executable code. */
has_code: z.boolean().default(false),
/** Section contains initialized data. */
has_initialized_data: z.boolean().default(false),
/** Section contains uninitialized data. */
has_uninitialized_data: z.boolean().default(false),
/** Section can be read. */
readable: z.boolean().default(false),
/** Section can be written. */
writable: z.boolean().default(false),
/** Section can be executed. */
executable: z.boolean().default(false),
});
/** PE/COFF section header. */
export const peSectionSchema = z.strictObject({
name: z.string().min(1),
virtual_size: z.number().int().nonnegative(),
virtual_address: z.number().int().nonnegative(),
raw_size: z.number().int().nonnegative(),
raw_offset: z.number().int().nonnegative(),
characteristics: sectionCharacteristicsSchema,
});
export type PeSection = z.infer<typeof peSectionSchema>;
/** PE import entry. */
export const peImportSchema = z.strictObject({
dll: z.string().min(1),
function_name: z.string().min(1),
ordinal: z.number().int().nullable(),
is_delay_import: z.boolean().default(false),
});
export type PeImport = z.infer<typeof peImportSchema>;
/** PE export entry. */
export const peExportSchema = z.strictObject({
name: z.string().min(1),
ordinal: z.number().int().nonnegative(),
rva: z.number().int().nonnegative(),
forwarder: z.string().nullable(),
});
export type PeExport = z.infer<typeof peExportSchema>;
/** PE/COFF resource entry. */
export const peResourceSchema = z.strictObject({
type: z.string().min(1),
name: z.string().nullable(),
language: z.number().int().nullable(),
rva: z.number().int().nonnegative(),
size: z.number().int().nonnegative(),
});
export type PeResource = z.infer<typeof peResourceSchema>;
/** PE debug directory entry. */
export const peDebugEntrySchema = z.strictObject({
type: z.string().min(1),
rva: z.number().int().nonnegative(),
size: z.number().int().nonnegative(),
/** PDB GUID if this is a CodeView entry. */
pdb_guid: z.string().nullable(),
/** PDB age if this is a CodeView entry. */
pdb_age: z.number().int().nullable(),
/** PDB path if this is a CodeView entry. */
pdb_path: z.string().nullable(),
});
export type PeDebugEntry = z.infer<typeof peDebugEntrySchema>;
/** TLS callback entry. */
export const peTlsCallbackSchema = z.strictObject({
callback_rva: z.number().int().nonnegative(),
index: z.number().int().nonnegative(),
});
export type PeTlsCallback = z.infer<typeof peTlsCallbackSchema>;
/** Architecture/machine type. */
export const peMachineTypeSchema = z.enum([
"x86",
"x64",
"arm",
"arm64",
"ia64",
"unknown",
]);
export type PeMachineType = z.infer<typeof peMachineTypeSchema>;
/** PE/COFF manifest for static inspection. */
export const peManifestSchema = z.strictObject({
/** DOS header magic. */
dos_magic: z.literal(0x5a4d),
/** PE signature. */
pe_signature: z.literal(0x00004550),
/** Machine type. */
machine: peMachineTypeSchema,
/** Number of sections. */
number_of_sections: z.number().int().positive(),
/** Timestamp from the COFF header. */
timestamp: z.number().int().nullable(),
/** PE characteristics flags. */
is_dll: z.boolean().default(false),
is_executable: z.boolean().default(false),
is_console: z.boolean().default(true),
/** Entry point RVA. */
entry_point: z.number().int().nonnegative(),
/** Image base. */
image_base: z.number().int().nonnegative(),
/** Section alignment. */
section_alignment: z.number().int().positive(),
/** Subsystem version. */
subsystem_version: z.string().nullable(),
/** Sections. */
sections: z.array(peSectionSchema).min(0).max(96),
/** Imports. */
imports: z.array(peImportSchema).default([]),
/** Exports. */
exports: z.array(peExportSchema).default([]),
/** Resources. */
resources: z.array(peResourceSchema).default([]),
/** Debug directory entries. */
debug_entries: z.array(peDebugEntrySchema).default([]),
/** TLS callbacks. */
tls_callbacks: z.array(peTlsCallbackSchema).default([]),
/** Relocations count (not all entries). */
relocations_count: z.number().int().nonnegative().default(0),
/** Image file characteristics. */
image_characteristics: z.array(z.string()).default([]),
});
export type PeManifest = z.infer<typeof peManifestSchema>;
/** Parse machine type from COFF machine field. */
export function parseMachineType(machine: number): PeMachineType {
switch (machine) {
case 0x014c:
return "x86";
case 0x8664:
return "x64";
case 0x01c0:
case 0x01c4:
return "arm";
case 0xaa64:
return "arm64";
case 0x0200:
return "ia64";
default:
return "unknown";
}
}
/** Parse section characteristics flags. */
export function parseSectionCharacteristics(
characteristics: number,
): z.infer<typeof sectionCharacteristicsSchema> {
const IMAGE_SCN_CNT_CODE = 0x00000020;
const IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040;
const IMAGE_SCN_CNT_UNINITIALIZED_DATA = 0x00000080;
const IMAGE_SCN_MEM_READ = 0x40000000;
const IMAGE_SCN_MEM_WRITE = 0x80000000;
const IMAGE_SCN_MEM_EXECUTE = 0x20000000;
return {
has_code: (characteristics & IMAGE_SCN_CNT_CODE) !== 0,
has_initialized_data:
(characteristics & IMAGE_SCN_CNT_INITIALIZED_DATA) !== 0,
has_uninitialized_data:
(characteristics & IMAGE_SCN_CNT_UNINITIALIZED_DATA) !== 0,
readable: (characteristics & IMAGE_SCN_MEM_READ) !== 0,
writable: (characteristics & IMAGE_SCN_MEM_WRITE) !== 0,
executable: (characteristics & IMAGE_SCN_MEM_EXECUTE) !== 0,
};
}
/** Parse image characteristics flags. */
export function parseImageCharacteristics(characteristics: number): string[] {
const IMAGE_DLL = 0x2000;
const IMAGE_EXECUTABLE = 0x0002;
const IMAGE_CONSOLE = 0x0003;
const flags: string[] = [];
if (characteristics & IMAGE_DLL) flags.push("DLL");
if (characteristics & IMAGE_EXECUTABLE) flags.push("Executable");
if ((characteristics & 0x0000000f) === IMAGE_CONSOLE) flags.push("Console");
return flags;
}
/** Parse a PE debug directory type. */
export function parseDebugType(type: number): string {
switch (type) {
case 0:
return "UNKNOWN";
case 1:
return "COFF";
case 2:
return "CODEVIEW";
case 3:
return "POGO";
case 4:
return "MISC";
default:
return "UNKNOWN";
}
}
/** Check if a PE manifest has PDB debug info. */
export function hasPdbInfo(manifest: PeManifest): boolean {
return manifest.debug_entries.some(
(e) => e.pdb_guid !== null && e.pdb_path !== null,
);
}
/** Get all imported DLLs from a PE manifest. */
export function getImportedDlls(manifest: PeManifest): string[] {
return [...new Set(manifest.imports.map((i) => i.dll))];
}
/** Get all exported function names from a PE manifest. */
export function getExportedFunctions(manifest: PeManifest): string[] {
return manifest.exports.map((e) => e.name);
}
/** Count executable sections. */
export function countExecutableSections(manifest: PeManifest): number {
return manifest.sections.filter((s) => s.characteristics.executable).length;
}
+272
View File
@@ -0,0 +1,272 @@
import { z } from "zod";
import { jsonValueSchema } from "./jsonValue.js";
/** Supported protocol families for structured protocol capture. */
export const protocolFamilySchema = z.enum([
"grpc",
"protobuf",
"json-rpc",
"messagepack",
]);
export type ProtocolFamily = z.infer<typeof protocolFamilySchema>;
/** A decoded field from a protocol message. */
export const decodedFieldSchema = z.strictObject({
/** Field name or path within the message structure. */
path: z.string().min(1),
/** Wire type or encoding hint. */
wire_type: z.string().nullable(),
/** Decoded field value. */
value: jsonValueSchema.nullable(),
/** Whether this field value was inferred rather than decoded. */
inferred: z.boolean().default(false),
});
export type DecodedField = z.infer<typeof decodedFieldSchema>;
/** Schema inference hypothesis for a repeated message pattern. */
export const schemaHypothesisSchema = z.strictObject({
/** Hypothesized field name. */
field_name: z.string().min(1),
/** Inferred type. */
inferred_type: z.string().min(1),
/** Confidence: 0-1 */
confidence: z.number().min(0).max(1),
/** Supporting evidence count. */
evidence_count: z.number().int().nonnegative(),
});
export type SchemaHypothesis = z.infer<typeof schemaHypothesisSchema>;
/** Result of decoding a single protocol message. */
export const protocolMessageSchema = z.strictObject({
/** Monotonic sequence number. */
sequence: z.number().int().nonnegative(),
/** Timestamp in milliseconds. */
at_ms: z.number().int().nonnegative(),
/** Protocol family. */
family: protocolFamilySchema,
/** Direction of the message. */
direction: z.enum(["request", "response"]),
/** Transport endpoint or path (e.g. gRPC method path). */
endpoint: z.string().nullable(),
/** Content type of the raw payload. */
content_type: z.string().nullable(),
/** Raw payload bytes as base64. */
raw_payload: z.string().nullable(),
/** Decoded fields from the payload. */
decoded_fields: z.array(decodedFieldSchema).default([]),
/** Inferred schema hypotheses. */
schema_hypotheses: z.array(schemaHypothesisSchema).default([]),
/** Whether decoding was truncated. */
truncated: z.boolean().default(false),
/** Whether credentials were detected and redacted. */
credentials_redacted: z.boolean().default(false),
});
export type ProtocolMessage = z.infer<typeof protocolMessageSchema>;
/** A captured protocol session with messages and inferred schemas. */
export const protocolCaptureSchema = z.strictObject({
/** Protocol family for this capture. */
family: protocolFamilySchema,
/** Captured messages in order. */
messages: z.array(protocolMessageSchema).min(0).max(10_000),
/** Aggregated schema hypotheses across all messages. */
inferred_schema: z.array(schemaHypothesisSchema).default([]),
/** Whether any message was truncated. */
has_truncated: z.boolean().default(false),
/** Whether credentials were detected and redacted. */
credentials_detected: z.boolean().default(false),
});
export type ProtocolCapture = z.infer<typeof protocolCaptureSchema>;
/** Classification of a byte sequence as a known protocol family. */
export function classifyProtocolFamily(
contentType: string | null,
path: string | null,
): ProtocolFamily | null {
if (contentType) {
if (contentType.includes("application/grpc")) return "grpc";
if (contentType.includes("application/x-protobuf")) return "protobuf";
if (contentType.includes("application/json-rpc")) return "json-rpc";
if (
contentType.includes("application/msgpack") ||
contentType.includes("application/x-msgpack")
)
return "messagepack";
}
if (path) {
if (path.startsWith("/grpc.") || path.includes("/grpc/")) return "grpc";
if (path.includes("/json-rpc") || path.includes("/jsonrpc"))
return "json-rpc";
}
return null;
}
/** Credential-like field names that should be redacted. */
const CREDENTIAL_PATTERNS = [
"password",
"passwd",
"secret",
"token",
"api_key",
"apikey",
"authorization",
"auth",
"credential",
"private_key",
];
/** Check if a field path looks like it may contain credentials. */
export function looksLikeCredential(path: string): boolean {
const lower = path.toLowerCase();
return CREDENTIAL_PATTERNS.some((p) => lower.includes(p));
}
/** Decode a JSON-RPC message from raw payload bytes. */
export function decodeJsonRpc(rawPayload: Uint8Array): {
decoded_fields: DecodedField[];
truncated: boolean;
} {
const text = new TextDecoder().decode(rawPayload);
const parsed = JSON.parse(text);
const fields: DecodedField[] = [];
if (typeof parsed === "object" && parsed !== null) {
for (const [key, value] of Object.entries(parsed)) {
if (looksLikeCredential(key)) {
fields.push({
path: key,
wire_type: "json",
value: "[REDACTED]",
inferred: false,
});
} else {
fields.push({
path: key,
wire_type: "json",
value: value as never,
inferred: false,
});
}
}
}
return { decoded_fields: fields, truncated: false };
}
/** Decode a MessagePack message (simplified: only checks basic structure). */
export function decodeMessagePack(rawPayload: Uint8Array): {
decoded_fields: DecodedField[];
truncated: boolean;
} {
const fields: DecodedField[] = [];
// MessagePack uses a single-byte type tag
if (rawPayload.length === 0) {
return { decoded_fields: [], truncated: false };
}
const tag = rawPayload[0]!;
// Map type: 0x80-0x8f (fixmap), 0xde (map16), 0xdf (map32)
if ((tag >= 0x80 && tag <= 0x8f) || tag === 0xde || tag === 0xdf) {
fields.push({
path: "type",
wire_type: "msgpack",
value: "map",
inferred: false,
});
}
// Array type: 0x90-0x9f (fixarray), 0xdc (array16), 0xdd (array32)
else if ((tag >= 0x90 && tag <= 0x9f) || tag === 0xdc || tag === 0xdd) {
fields.push({
path: "type",
wire_type: "msgpack",
value: "array",
inferred: false,
});
} else {
fields.push({
path: "type",
wire_type: "msgpack",
value: "unknown",
inferred: true,
});
}
return {
decoded_fields: fields,
truncated: rawPayload.length > 10_000,
};
}
/** Decode a gRPC frame (5-byte header + protobuf payload). */
export function decodeGrpcFrame(rawPayload: Uint8Array): {
decoded_fields: DecodedField[];
truncated: boolean;
} {
// gRPC uses a 5-byte prefix: 1 byte compressed flag + 4 byte length
if (rawPayload.length < 5) {
return {
decoded_fields: [
{
path: "error",
wire_type: "grpc",
value: "frame too short",
inferred: false,
},
],
truncated: false,
};
}
const compressed = rawPayload[0] !== 0;
const length =
(rawPayload[1]! << 24) |
(rawPayload[2]! << 16) |
(rawPayload[3]! << 8) |
rawPayload[4]!;
return {
decoded_fields: [
{
path: "compressed",
wire_type: "grpc",
value: compressed,
inferred: false,
},
{
path: "message_length",
wire_type: "grpc",
value: length,
inferred: false,
},
],
truncated: length > 10_000,
};
}
/** Infer schema hypotheses from a set of decoded messages. */
export function inferMessageSchema(
messages: readonly ProtocolMessage[],
): SchemaHypothesis[] {
const fieldCounts = new Map<string, { count: number; types: Set<string> }>();
for (const msg of messages) {
for (const field of msg.decoded_fields) {
const existing = fieldCounts.get(field.path);
if (existing) {
existing.count++;
if (field.wire_type) existing.types.add(field.wire_type);
} else {
fieldCounts.set(field.path, {
count: 1,
types: new Set(field.wire_type ?? "unknown"),
});
}
}
}
const hypotheses: SchemaHypothesis[] = [];
for (const [field_name, data] of fieldCounts) {
hypotheses.push({
field_name,
inferred_type: [...data.types].join("|"),
confidence: Math.min(1, data.count / messages.length || 0),
evidence_count: data.count,
});
}
return hypotheses;
}
+56 -8
View File
@@ -1,4 +1,5 @@
import { createHash } from "node:crypto";
import { createReadStream } from "node:fs";
import { readFile, stat } from "node:fs/promises";
import {
@@ -85,6 +86,8 @@ export class ManagedStaticProvider implements AnalysisProvider {
}
class ManagedStaticClient implements AnalysisClient {
#snapshotBytes: Buffer | undefined;
constructor(private readonly target: BinaryTarget) {}
async execute(
@@ -123,11 +126,12 @@ class ManagedStaticClient implements AnalysisClient {
`Artifact size ${String(metadata.size)} exceeds max_file_bytes ${String(maxFileBytes)}.`,
),
);
const bytes = await readFile(
this.target.path,
options?.signal === undefined ? undefined : { signal: options.signal },
);
if (bytes.length > maxFileBytes)
const snapshot = this.#snapshotBytes;
const observed =
snapshot === undefined
? await readManagedSnapshot(this.target.path, options?.signal)
: await hashManagedSource(this.target.path, options?.signal);
if (observed.byteLength > maxFileBytes)
return err(
new AnalysisCapabilityUnavailableError(
IDENTITY.id,
@@ -135,13 +139,16 @@ class ManagedStaticClient implements AnalysisClient {
`Artifact grew beyond max_file_bytes ${String(maxFileBytes)} while it was read.`,
),
);
const observedDigest = createHash("sha256").update(bytes).digest("hex");
if (observedDigest !== this.target.sha256)
if (observed.sha256 !== this.target.sha256)
return err(
new EvidenceIntegrityError(
`Managed artifact digest changed after open: expected ${this.target.sha256}, observed ${observedDigest} at ${this.target.path}`,
`Managed artifact digest changed after open: expected ${this.target.sha256}, observed ${observed.sha256} at ${this.target.path}`,
),
);
const bytes = snapshot ?? observed.bytes;
if (bytes === undefined)
throw new TypeError("Managed snapshot bytes are unavailable");
this.#snapshotBytes = bytes;
const result = inspectManagedOperation(
operation,
parameters,
@@ -164,10 +171,51 @@ class ManagedStaticClient implements AnalysisClient {
}
close(): Promise<void> {
this.#snapshotBytes = undefined;
return Promise.resolve();
}
}
interface ManagedSourceObservation {
readonly byteLength: number;
readonly sha256: string;
readonly bytes?: Buffer;
}
const readManagedSnapshot = async (
path: string,
signal?: AbortSignal,
): Promise<ManagedSourceObservation> => {
const bytes = await readFile(
path,
signal === undefined ? undefined : { signal },
);
return {
byteLength: bytes.length,
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes,
};
};
const hashManagedSource = async (
path: string,
signal?: AbortSignal,
): Promise<ManagedSourceObservation> => {
const digest = createHash("sha256");
let byteLength = 0;
const stream = createReadStream(
path,
signal === undefined ? undefined : { signal },
);
for await (const chunk of stream) {
if (!Buffer.isBuffer(chunk))
throw new TypeError("Managed source stream returned non-buffer data");
byteLength += chunk.length;
digest.update(chunk);
}
return { byteLength, sha256: digest.digest("hex") };
};
const isManagedOperation = (
operation: AnalysisOperation,
): operation is ManagedToolName =>
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,7 +1,7 @@
/** Generated from package.json and package-lock.json; do not edit. */
export const PACKAGE_METADATA = {
name: "rea-agents",
version: "2.5.0", // x-release-please-version
version: "2.7.0", // x-release-please-version
serverSdkVersion: "2.0.0-beta.4",
clientSdkVersion: "2.0.0-beta.4",
coreSdkVersion: "2.0.0-beta.4",
+1 -1
View File
@@ -17,4 +17,4 @@ export const GHIDRA_MAX_QUEUED_REQUESTS = 32;
/** Maximum encoded response line accepted from the Java bridge. */
export const GHIDRA_MAX_LINE_BYTES = 1024 * 1024;
/** Version of REA's Java bridge handshake and request protocol. */
export const GHIDRA_BRIDGE_VERSION = 5;
export const GHIDRA_BRIDGE_VERSION = 6;
+35 -3
View File
@@ -11,6 +11,7 @@ import {
jsonValueSchema,
type JsonValue,
} from "../domain/jsonValue.js";
import { nativeApiBoundarySchema } from "../domain/nativeApiBoundary.js";
import { err, ok, type Result } from "../domain/result.js";
import {
ghidraBoundedIdentifierSchema,
@@ -220,8 +221,40 @@ const procedureInfo = z
})
.strict();
const ghidraFunctionDossier = functionDossierSchema.superRefine(
const ghidraNativeApiBoundary = nativeApiBoundarySchema.superRefine(
(value, context) => {
if (!value.available) return;
if (value.provenance !== "ghidra-high-function")
context.addIssue({
code: "custom",
message:
"Ghidra native API observations require HighFunction provenance",
});
const addresses = value.jump_tables.flatMap((table) => [
table.dispatch_address,
...table.data_sources.map(({ address }) => address),
...table.mappings.flatMap(({ target_address, data_addresses }) => [
target_address,
...data_addresses,
]),
]);
if (
addresses.some(
(address) => !ghidraCanonicalAddressSchema.safeParse(address).success,
)
)
context.addIssue({
code: "custom",
message:
"Ghidra native API observation contains a non-canonical address",
});
},
);
const ghidraFunctionDossier = functionDossierSchema
.extend({ native_api: ghidraNativeApiBoundary })
.strict()
.superRefine((value, context) => {
const addresses = [
value.procedure.address,
...value.comments.items.map(({ address }) => address),
@@ -288,8 +321,7 @@ const ghidraFunctionDossier = functionDossierSchema.superRefine(
code: "custom",
message: "Ghidra dossier omitted required uncertainty limitations",
});
},
);
});
const ghidraFunctionInstructionWindow = functionInstructionWindowSchema
.extend({ procedure: procedureIdentity })
.strict();
+12 -13
View File
@@ -2,10 +2,7 @@ import type {
CapabilityDescriptor,
ProviderIdentity,
} from "../application/AnalysisProvider.js";
import {
ENHANCED_TOOL_CONTRACTS,
OFFICIAL_TOOL_CONTRACTS,
} from "../contracts/toolContracts.js";
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
import {
GHIDRA_DECOMPILE_REQUEST_TIMEOUT_MS,
GHIDRA_MAX_LINE_BYTES,
@@ -22,10 +19,9 @@ export const GHIDRA_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
});
const providerContractByName = new Map(
[...OFFICIAL_TOOL_CONTRACTS, ...ENHANCED_TOOL_CONTRACTS].map((contract) => [
contract.name,
contract,
]),
GENERATED_MCP_TOOL_CATALOG.filter(
({ kind }) => kind === "official-proxy" || kind === "enhanced",
).map((contract) => [contract.name, contract]),
);
/** Provider-neutral read-only contracts implemented by the Ghidra adapter. */
@@ -165,10 +161,13 @@ export const limitationsFor = (operation: string): readonly string[] => {
/** Provider-neutral capabilities advertised by every non-Windows Ghidra session. */
export const CAPABILITIES: readonly CapabilityDescriptor[] = Object.freeze(
GHIDRA_PROVIDER_TOOL_CONTRACTS.map((contract) =>
Object.freeze({
GHIDRA_PROVIDER_TOOL_CONTRACTS.map((contract) => {
const operation = contract.analysisOperation;
if (operation === null)
throw new TypeError(`Missing analysis operation for ${contract.name}`);
return Object.freeze({
provider: GHIDRA_PROVIDER_IDENTITY,
operation: contract.name,
operation,
inputContractVersion: 1,
outputContractVersion: 1,
available: true,
@@ -198,8 +197,8 @@ export const CAPABILITIES: readonly CapabilityDescriptor[] = Object.freeze(
: GHIDRA_REQUEST_TIMEOUT_MS,
}),
limitations: Object.freeze(limitationsFor(contract.name)),
}),
),
});
}),
);
/** Capabilities advertised for the experimental Windows x64 P0 boundary. */
+32 -3
View File
@@ -42,7 +42,11 @@ import {
type HopperRequestActivity,
} from "./HopperRequestQueue.js";
import { HopperResponseStream } from "./HopperResponseStream.js";
import { responseResult } from "./protocol.js";
import {
type HopperBridgeEvent,
type HopperBridgeMessage,
responseResult,
} from "./protocol.js";
export type { HopperServerInfo } from "./HopperSessionValues.js";
@@ -125,8 +129,7 @@ export class HopperClient {
},
);
this.#responses = new HopperResponseStream({
accept: (response) =>
this.#requests.accept(response.id, responseResult(response)),
accept: (message) => this.#acceptBridgeMessage(message),
hasQueued: (id) => this.#requests.hasQueued(id),
nextRequestId: () => this.#nextId,
abort: (message, cause) => this.#abortProtocol(message, cause),
@@ -450,6 +453,32 @@ export class HopperClient {
socket.on("close", this.#onSocketClose);
}
#acceptBridgeMessage(message: HopperBridgeMessage): boolean {
if (!("event" in message))
return this.#requests.accept(message.id, responseResult(message));
if (!this.#requests.acceptEvent(message)) return false;
this.#emitBridgeDiagnostic(message);
return true;
}
#emitBridgeDiagnostic(message: HopperBridgeEvent): void {
if (message.event.type !== "diagnostic") return;
try {
this.#options.onDiagnostic?.({
type: "bridge-diagnostic",
request_id: message.id,
code: message.event.error.code,
category: message.event.error.type,
message: message.event.error.message,
});
} catch {
this.#logger.warn(
{ requestId: message.id },
"Hopper bridge diagnostic consumer rejected an event",
);
}
}
#detachSocket(socket: Socket): void {
socket.off("data", this.#onSocketData);
socket.off("error", this.#onSocketError);
+8
View File
@@ -1,3 +1,4 @@
import type { HopperDiagnosticType } from "../domain/errors.js";
import type { ProcessCleanupResult } from "../process/ProcessOwnership.js";
import type { ProviderProcessStopResult } from "../process/ProviderProcess.js";
import type { BridgeLaunch } from "./BridgeLauncher.js";
@@ -6,6 +7,13 @@ import type { BridgeLaunch } from "./BridgeLauncher.js";
export type HopperDiagnostic =
| { readonly type: "launcher-stderr"; readonly bytes: number }
| { readonly type: "launcher-exit"; readonly code: number | null }
| {
readonly type: "bridge-diagnostic";
readonly request_id: number;
readonly code: number;
readonly category: HopperDiagnosticType;
readonly message: string;
}
| {
readonly type: "owned-shutdown";
readonly status: ProviderProcessStopResult["status"];
+23 -13
View File
@@ -18,10 +18,7 @@ import type { BinaryTarget } from "../domain/binaryTarget.js";
import type { Logger } from "../logger.js";
import { AnalysisCapabilityUnavailableError } from "../domain/errors.js";
import { err } from "../domain/result.js";
import {
ENHANCED_TOOL_CONTRACTS,
OFFICIAL_TOOL_CONTRACTS,
} from "../contracts/toolContracts.js";
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
import { HopperApplicationLauncher } from "./BridgeLauncher.js";
import {
hopperLoaderArgsForTarget,
@@ -47,22 +44,29 @@ const MUTATING_OPERATIONS = new Set([
/** Tool contracts implemented directly by the Hopper adapter. */
export const HOPPER_PROVIDER_TOOL_CONTRACTS = Object.freeze([
...OFFICIAL_TOOL_CONTRACTS,
...ENHANCED_TOOL_CONTRACTS.filter(
(contract) => contract.name === "analyze_function",
...GENERATED_MCP_TOOL_CATALOG.filter(({ kind }) => kind === "official-proxy"),
...GENERATED_MCP_TOOL_CATALOG.filter(
({ name }) => name === "analyze_function",
),
]);
const CAPABILITIES: readonly CapabilityDescriptor[] = Object.freeze(
HOPPER_PROVIDER_TOOL_CONTRACTS.map((contract) =>
Object.freeze({
HOPPER_PROVIDER_TOOL_CONTRACTS.map((contract) => {
const operation = contract.analysisOperation;
if (operation === null)
throw new TypeError(`Missing analysis operation for ${contract.name}`);
return Object.freeze({
provider: IDENTITY,
operation: contract.name,
operation,
inputContractVersion: 1,
outputContractVersion: 1,
available: true,
reason: null,
pagination: "offset" in contract.inputSchema.shape ? "offset" : "none",
pagination:
isRecord(contract.inputSchema.properties) &&
"offset" in contract.inputSchema.properties
? "offset"
: "none",
exhaustive: false,
effects: Object.freeze({
mutatesArtifact: MUTATING_OPERATIONS.has(contract.name),
@@ -81,10 +85,14 @@ const CAPABILITIES: readonly CapabilityDescriptor[] = Object.freeze(
limitations: Object.freeze([
"Results depend on Hopper's completed static analysis.",
]),
}),
),
});
}),
);
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
/** Concrete analysis provider backed by REA's private Hopper bridge. */
export class HopperProvider implements AnalysisProviderCandidate {
constructor(
@@ -233,6 +241,8 @@ export class HopperProvider implements AnalysisProviderCandidate {
}),
...(context === undefined ? {} : { runId: context.runId }),
logger: this.logger.child({ layer: "bridge" }),
onDiagnostic: (diagnostic) =>
this.logger.info(diagnostic, "Hopper bridge reported a diagnostic"),
});
return {
execute: async (operation, parameters, options) => {
+43 -9
View File
@@ -1,6 +1,10 @@
import { performance } from "node:perf_hooks";
import type { ProgressReporter } from "../application/ProgressReporter.js";
import type {
ProgressReporter,
ProgressUpdate,
} from "../application/ProgressReporter.js";
import type { HopperBridgeEvent } from "./protocol.js";
import {
HopperCancelledError,
type HopperError,
@@ -127,6 +131,28 @@ export class HopperRequestQueue {
return true;
}
/** Forward one correlated bridge event without releasing the wire request. */
acceptEvent(event: HopperBridgeEvent): boolean {
const entry = this.#active;
if (entry === undefined || entry.id !== event.id) return false;
if (event.event.type === "progress" && !entry.callerSettled) {
if (event.event.terminal === true && entry.heartbeat !== undefined) {
clearInterval(entry.heartbeat);
entry.heartbeat = undefined;
}
this.#reportUpdate(entry, {
phase: event.event.phase,
completed: event.event.completed,
total: event.event.total,
message: event.event.message,
...(event.event.terminal === undefined
? {}
: { terminal: event.event.terminal }),
});
}
return true;
}
/** Fail the active wire request and every request still waiting in the FIFO. */
failAll(error: HopperError): void {
const active = this.#active;
@@ -267,13 +293,21 @@ export class HopperRequestQueue {
}
#report(entry: QueuedRequest, message: string): void {
void entry.progress
?.report({
phase: "hopper_request",
completed: 0,
total: 1,
message,
})
.catch(() => undefined);
this.#reportUpdate(entry, {
phase: "hopper_request",
completed: 0,
total: 1,
message,
});
}
#reportUpdate(entry: QueuedRequest, update: ProgressUpdate): void {
try {
void Promise.resolve(entry.progress?.report(update)).catch(
() => undefined,
);
} catch {
// Progress observation cannot change the request outcome.
}
}
}
+6 -3
View File
@@ -1,9 +1,12 @@
import { parseResponseLine, type HopperResponse } from "./protocol.js";
import {
parseBridgeMessageLine,
type HopperBridgeMessage,
} from "./protocol.js";
const MAX_LINE_BYTES = 10 * 1024 * 1024;
export interface HopperResponseStreamOptions {
readonly accept: (response: HopperResponse) => boolean;
readonly accept: (message: HopperBridgeMessage) => boolean;
readonly hasQueued: (id: number) => boolean;
readonly nextRequestId: () => number;
readonly abort: (message: string, cause?: Error) => void;
@@ -38,7 +41,7 @@ export class HopperResponseStream {
}
#acceptLine(line: string): boolean {
const parsed = parseResponseLine(line);
const parsed = parseBridgeMessageLine(line);
if (!parsed.ok) {
this.options.abort(parsed.error.message, parsed.error);
return false;
+75 -26
View File
@@ -4,35 +4,84 @@ import { HopperProtocolError, HopperRemoteError } from "../domain/errors.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js";
const remoteErrorSchema = z
.object({
code: z.number().int(),
message: z.string().max(512),
type: z
.enum([
"remote",
"authorization",
"invalid_request",
"capability_unavailable",
"bridge_exception",
])
.default("remote"),
})
.strict();
const responseSchema = z.union([
z.object({
id: z.number().int().nonnegative(),
result: jsonValueSchema,
}),
z.object({
id: z.number().int().nonnegative(),
error: z.object({
code: z.number().int(),
message: z.string(),
type: z
.enum([
"remote",
"authorization",
"invalid_request",
"capability_unavailable",
"bridge_exception",
])
.default("remote"),
}),
}),
z
.object({
id: z.number().int().nonnegative(),
result: jsonValueSchema,
})
.strict(),
z
.object({
id: z.number().int().nonnegative(),
error: remoteErrorSchema,
})
.strict(),
]);
export type HopperResponse = z.infer<typeof responseSchema>;
const eventSchema = z
.object({
id: z.number().int().nonnegative(),
event: z.discriminatedUnion("type", [
z
.object({
type: z.literal("progress"),
phase: z.string().min(1).max(64),
completed: z.number().nonnegative().finite(),
total: z.number().nonnegative().finite().nullable(),
message: z.string().min(1).max(512),
terminal: z.boolean().optional(),
})
.strict(),
z
.object({
type: z.literal("diagnostic"),
error: remoteErrorSchema,
})
.strict(),
]),
})
.strict()
.superRefine((message, context) => {
if (
message.event.type === "progress" &&
message.event.total !== null &&
message.event.completed > message.event.total
) {
context.addIssue({
code: "custom",
message: "Progress total cannot be less than completed",
path: ["event", "total"],
});
}
});
/** Parse one complete Hopper NDJSON response line. */
export const parseResponseLine = (
const messageSchema = z.union([responseSchema, eventSchema]);
export type HopperResponse = z.infer<typeof responseSchema>;
export type HopperBridgeEvent = z.infer<typeof eventSchema>;
export type HopperBridgeMessage = z.infer<typeof messageSchema>;
/** Parse one complete Hopper NDJSON response or event line. */
export const parseBridgeMessageLine = (
line: string,
): Result<HopperResponse, HopperProtocolError> => {
): Result<HopperBridgeMessage, HopperProtocolError> => {
let decoded: unknown;
try {
decoded = JSON.parse(line);
@@ -42,12 +91,12 @@ export const parseResponseLine = (
);
}
const parsed = responseSchema.safeParse(decoded);
const parsed = messageSchema.safeParse(decoded);
return parsed.success
? ok(parsed.data)
: err(
new HopperProtocolError(
"Hopper returned a response outside the bridge contract",
"Hopper returned a message outside the bridge contract",
{ cause: parsed.error },
),
);
+19 -12
View File
@@ -10,10 +10,6 @@ import { createBinarySession } from "./application/runtime.js";
import { createLogger } from "./logger.js";
import { projectAnalysisError } from "./domain/errors.js";
import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js";
import { CdpBrowserProvider } from "./browser/CdpBrowserProvider.js";
import { CdpElectronProvider } from "./browser/CdpElectronProvider.js";
import { V8InspectorProvider } from "./browser/V8InspectorProvider.js";
import { PlaywrightBrowserScenarioProvider } from "./browser/PlaywrightBrowserScenarioProvider.js";
import type { RuntimeDependencies } from "./main/types.js";
import { SERVER_START_FAILED } from "./main/messages.js";
import { createRuntimeState } from "./main/state.js";
@@ -73,10 +69,6 @@ export const run = async (
}
const session = createBinarySession(config.value, logger);
const browserObservation = new CdpBrowserProvider();
const browserScenarioCapture = new PlaywrightBrowserScenarioProvider();
const electronObservation = new CdpElectronProvider();
const javascriptRuntimeObservation = new V8InspectorProvider();
const opened = await openInitialTarget(
session,
config.value,
@@ -88,10 +80,25 @@ export const run = async (
const transport = await startMcpTransport(dependencies, session, {
logger,
serverLogger,
browserObservation,
browserScenarioCapture,
electronObservation,
javascriptRuntimeObservation,
loadOptionalProviders: async () => {
const [
{ CdpBrowserProvider },
{ PlaywrightBrowserScenarioProvider },
{ CdpElectronProvider },
{ V8InspectorProvider },
] = await Promise.all([
import("./browser/CdpBrowserProvider.js"),
import("./browser/PlaywrightBrowserScenarioProvider.js"),
import("./browser/CdpElectronProvider.js"),
import("./browser/V8InspectorProvider.js"),
]);
return {
browserObservation: new CdpBrowserProvider(),
browserScenarioCapture: new PlaywrightBrowserScenarioProvider(),
electronObservation: new CdpElectronProvider(),
javascriptRuntimeObservation: new V8InspectorProvider(),
};
},
permissionAuthority: permissionAuthority.value,
runtimeState,
});
+8 -14
View File
@@ -1,13 +1,12 @@
import type { StdioServerHandle } from "@modelcontextprotocol/server/stdio";
import type { BinarySession } from "../application/BinarySession.js";
import type { CdpBrowserProvider } from "../browser/CdpBrowserProvider.js";
import type { CdpElectronProvider } from "../browser/CdpElectronProvider.js";
import type { V8InspectorProvider } from "../browser/V8InspectorProvider.js";
import type { PlaywrightBrowserScenarioProvider } from "../browser/PlaywrightBrowserScenarioProvider.js";
import type { PermissionAuthority } from "../application/PermissionAuthority.js";
import type { Logger } from "../logger.js";
import { createServer } from "../server/createServer.js";
import {
createServer,
type CreateServerOptions,
} from "../server/createServer.js";
import type { RuntimeDependencies } from "./types.js";
import type { RuntimeState } from "./state.js";
import {
@@ -20,10 +19,9 @@ type McpServerInstance = ReturnType<typeof createServer>;
interface ServerContext {
readonly logger: Logger;
readonly serverLogger: Logger;
readonly browserObservation: CdpBrowserProvider;
readonly browserScenarioCapture: PlaywrightBrowserScenarioProvider;
readonly electronObservation: CdpElectronProvider;
readonly javascriptRuntimeObservation: V8InspectorProvider;
readonly loadOptionalProviders: NonNullable<
CreateServerOptions["loadOptionalProviders"]
>;
readonly permissionAuthority: PermissionAuthority;
readonly runtimeState: RuntimeState;
}
@@ -58,11 +56,7 @@ export const startMcpTransport = async (
analysisSnapshotFilePolicy:
serverContext.runtimeState.snapshotPolicy,
permissionAuthority: serverContext.permissionAuthority,
browserObservation: serverContext.browserObservation,
browserScenarioCapture: serverContext.browserScenarioCapture,
electronObservation: serverContext.electronObservation,
javascriptRuntimeObservation:
serverContext.javascriptRuntimeObservation,
loadOptionalProviders: serverContext.loadOptionalProviders,
artifactIntegrityContinueEnabled: () =>
serverContext.runtimeState.currentConfig
.artifactIntegrityContinueEnabled,
+3 -2
View File
@@ -10,8 +10,8 @@ import {
} from "./catalogIdentity.js";
import { PROMPT_CONTRACTS } from "./contracts/promptContracts.js";
import { PRODUCT_IDENTITY } from "./identity.js";
import { MCP_STARTUP_POLICY } from "./mcpStartupPolicy.js";
const DEFAULT_DEADLINE_MS = 20_000;
const STDERR_LIMIT_BYTES = 64 * 1_024;
const OUTPUT_FORMATS = ["toon", "json", "yaml", "md", "jsonl"] as const;
type OutputFormat = (typeof OUTPUT_FORMATS)[number];
@@ -40,7 +40,8 @@ interface McpDoctorCheck {
/** Diagnose the actual production stdio adapter with one bounded SDK session. */
export const runProductionMcpDoctor = async (options: McpDoctorOptions) => {
const deadline = Date.now() + (options.deadlineMs ?? DEFAULT_DEADLINE_MS);
const deadline =
Date.now() + (options.deadlineMs ?? MCP_STARTUP_POLICY.doctorDeadlineMs);
const controller = new AbortController();
const timer = setTimeout(
() => controller.abort(new Error("Production MCP doctor deadline expired")),
+14
View File
@@ -0,0 +1,14 @@
/**
* Supported MCP startup deadlines and regression budgets.
*
* The Codex timeout is caller-facing configuration. The smaller phase budgets
* preserve operational headroom, while the doctor deadline covers additional
* inventory requests after initialization.
*/
export const MCP_STARTUP_POLICY = {
codexStartupTimeoutSeconds: 30,
initializeBudgetMs: 15_000,
toolsListBudgetMs: 5_000,
firstCatalogBudgetMs: 20_000,
doctorDeadlineMs: 30_000,
} as const;
+181
View File
@@ -0,0 +1,181 @@
import type {
AnyToolHandler,
JsonSchemaValidator,
jsonSchemaValidator,
McpServer,
RegisteredTool,
StandardSchemaWithJSON,
} from "@modelcontextprotocol/server";
import {
fromJsonSchema,
isCallToolResult,
isInputRequiredResult,
} from "@modelcontextprotocol/server";
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
import type { ToolKind } from "../contracts/toolContractTypes.js";
type HydrateToolRegistrations = (kind: ToolKind) => Promise<void>;
/**
* Advertise the generated MCP catalog immediately and hydrate its callbacks
* through the existing registrars on first execution.
*/
export class LazyToolCatalog {
readonly #server: McpServer;
readonly #hydrateRegistrations: HydrateToolRegistrations;
readonly #includeSessionTools: boolean;
readonly #tools = new Map<string, RegisteredTool>();
readonly #hydrations = new Map<ToolKind, Promise<void>>();
#hydrationQueue: Promise<void> = Promise.resolve();
#closed = false;
constructor(
server: McpServer,
hydrateRegistrations: HydrateToolRegistrations,
includeSessionTools = true,
) {
this.#server = server;
this.#hydrateRegistrations = hydrateRegistrations;
this.#includeSessionTools = includeSessionTools;
}
/** Register every generated tool with a lazy callback. */
register(): void {
for (const contract of GENERATED_MCP_TOOL_CATALOG) {
if (contract.requiresSession && !this.#includeSessionTools) continue;
const inputSchema = advertisedSchema(contract.inputSchema);
const outputSchema = advertisedSchema(contract.outputSchema);
let registered: RegisteredTool | undefined;
const callback: AnyToolHandler<StandardSchemaWithJSON> = async (
input,
context,
) => {
await this.#hydrate(contract.kind);
const handler = registered?.handler;
if (handler === undefined || handler === callback)
throw new Error(`MCP tool ${contract.name} was not hydrated`);
const result: unknown = await Reflect.apply(handler, undefined, [
input,
context,
]);
if (isCallToolResult(result) || isInputRequiredResult(result))
return result;
throw new TypeError(
`MCP tool ${contract.name} returned an invalid result`,
);
};
registered = this.#server.registerTool(
contract.name,
{
title: contract.title,
description: contract.description,
inputSchema,
outputSchema,
annotations: contract.annotations,
},
callback,
);
this.#tools.set(contract.name, registered);
}
}
/** Wait for in-flight hydration and reject future first-use loading. */
async close(): Promise<void> {
this.#closed = true;
await Promise.allSettled(this.#hydrations.values());
}
async #hydrate(kind: ToolKind): Promise<void> {
if (this.#closed) throw new Error("MCP tool catalog is closed");
const existing = this.#hydrations.get(kind);
if (existing !== undefined) {
await existing;
return;
}
const hydration = this.#hydrationQueue
.catch(() => undefined)
.then(() => this.#runHydration(kind));
this.#hydrationQueue = hydration;
this.#hydrations.set(kind, hydration);
try {
await hydration;
} catch (cause: unknown) {
if (this.#hydrations.get(kind) === hydration)
this.#hydrations.delete(kind);
throw cause;
}
}
async #runHydration(kind: ToolKind): Promise<void> {
if (this.#closed) throw new Error("MCP tool catalog is closed");
const expected = GENERATED_MCP_TOOL_CATALOG.filter(
(contract) =>
contract.kind === kind &&
(this.#includeSessionTools || !contract.requiresSession),
).map((contract) => contract.name);
const expectedNames = new Set(expected);
const hydrated = new Set<string>();
const originalRegisterTool = this.#server.registerTool;
const hydrationRegisterTool = (...args: unknown[]): unknown => {
const [name, config, callback] = args;
if (
typeof name !== "string" ||
!isRecord(config) ||
typeof callback !== "function"
)
throw new TypeError("Invalid lazy MCP tool registration");
const registered = this.#tools.get(name);
if (registered === undefined || !expectedNames.has(name))
throw new TypeError(
`MCP tool ${name} is not in the ${kind} catalog family`,
);
if (hydrated.has(name))
throw new TypeError(`MCP tool ${name} was hydrated more than once`);
hydrated.add(name);
Reflect.apply(registered.update, registered, [
{
title: config.title,
description: config.description,
paramsSchema: config.inputSchema,
outputSchema: config.outputSchema,
annotations: config.annotations,
callback,
},
]);
return registered;
};
if (!Reflect.set(this.#server, "registerTool", hydrationRegisterTool))
throw new TypeError("Unable to install lazy MCP tool hydration");
try {
await this.#hydrateRegistrations(kind);
} finally {
Reflect.set(this.#server, "registerTool", originalRegisterTool);
}
const missing = expected.filter((name) => !hydrated.has(name));
if (missing.length > 0)
throw new TypeError(
`Generated MCP tools were not hydrated: ${missing.join(", ")}`,
);
}
}
const advertisedSchema = (
jsonSchema: Readonly<Record<string, unknown>>,
): StandardSchemaWithJSON =>
fromJsonSchema(jsonSchema, DEFERRED_VALIDATION_PROVIDER);
class DeferredValidationProvider implements jsonSchemaValidator {
getValidator<T>(): JsonSchemaValidator<T> {
return (input: unknown) => ({
valid: true,
data: input as T,
errorMessage: undefined,
});
}
}
const DEFERRED_VALIDATION_PROVIDER = new DeferredValidationProvider();
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
typeof value === "object" && value !== null && !Array.isArray(value);
+85 -204
View File
@@ -10,29 +10,16 @@ import {
import type { AnalysisOperationPort } from "../application/AnalysisProvider.js";
import type { BinarySessionPort } from "../application/BinarySession.js";
import { PRODUCT_IDENTITY } from "../identity.js";
import { registerEnhancedTools } from "./registerEnhancedTools.js";
import { registerOfficialTools } from "./registerOfficialTools.js";
import { registerSessionTools } from "./registerSessionTools.js";
import { registerNativeTools } from "./registerNativeTools.js";
import { registerArtifactTools } from "./registerArtifactTools.js";
import { registerManagedTools } from "./registerManagedTools.js";
import { registerManagedWorkflowTools } from "./registerManagedWorkflowTools.js";
import { silentLogger, type Logger } from "../logger.js";
import type { ProcessExecutionPolicy } from "../domain/processCapture.js";
import type { EvidenceFilePolicy } from "../domain/evidenceBundle.js";
import { registerGuidedPrompts } from "./registerPrompts.js";
import { registerEvidenceResources } from "./registerEvidenceResources.js";
import { createServerIdentity } from "../serverIdentity.js";
import type { PermissionAuthority } from "../application/PermissionAuthority.js";
import type { BrowserObservationPort } from "../application/BrowserObservationPort.js";
import type { BrowserScenarioCapturePort } from "../application/BrowserScenarioCapturePort.js";
import { registerBrowserTools } from "./registerBrowserTools.js";
import { registerBrowserScenarioTool } from "./registerBrowserScenarioTool.js";
import type { ElectronObservationPort } from "../application/ElectronObservationPort.js";
import { registerElectronTools } from "./registerElectronTools.js";
import type { JavaScriptRuntimeObservationPort } from "../application/JavaScriptRuntimeObservationPort.js";
import { registerJavaScriptRuntimeObservationTools } from "./registerJavaScriptRuntimeObservationTools.js";
import { registerApplicationTools } from "./registerApplicationTools.js";
import type { SessionAvailability } from "./sessionAvailabilityPolicy.js";
import { sessionAvailabilityPolicy } from "./sessionAvailabilityPolicy.js";
import {
@@ -52,12 +39,11 @@ import type {
JavaScriptReplayRunner,
} from "../application/JavaScriptReplayPlanning.js";
import type { ManagedRuntimePolicy } from "../application/ManagedRuntimeCorrelationService.js";
import { LinuxJavaScriptReplayRunner } from "../replay/LinuxJavaScriptReplayRunner.js";
import { SystemJavaScriptReplayHost } from "../replay/SystemJavaScriptReplayHost.js";
import {
PROCESS_CAPTURE_ELICITATION_POLICY,
type ProcessCaptureElicitationState,
} from "./ProcessCaptureElicitation.js";
import { LazyToolCatalog } from "./LazyToolCatalog.js";
export interface CreateServerOptions {
readonly logger?: Logger;
@@ -76,6 +62,12 @@ export interface CreateServerOptions {
readonly javascriptReplayRunner?: JavaScriptReplayRunner;
readonly managedRuntimePolicy?: ManagedRuntimePolicy;
readonly availabilityPolicy?: () => SessionAvailability;
readonly loadOptionalProviders?: () => Promise<{
readonly browserObservation: BrowserObservationPort;
readonly browserScenarioCapture: BrowserScenarioCapturePort;
readonly electronObservation: ElectronObservationPort;
readonly javascriptRuntimeObservation: JavaScriptRuntimeObservationPort;
}>;
}
const installSessionToolAvailability = (
@@ -155,51 +147,88 @@ export const createServer = (
const toolLogger = logger.child({ layer: "server" });
const { activeTarget, recordEvidence, recordEvidenceWithUnknown } =
createSessionRecorders(server, session);
const toolContext = {
server,
analysis,
session,
options,
logger: toolLogger,
permissionAuthority,
activeTarget,
recordEvidence,
recordEvidenceWithUnknown,
const processCaptureElicitation = {
stateCodec: processCaptureStateCodec,
supported: (context: {
readonly mcpReq: {
readonly envelope?: Readonly<Record<string, unknown>>;
};
}) => {
const envelope = context.mcpReq.envelope;
const version = envelope?.[PROTOCOL_VERSION_META_KEY];
const capabilities = envelope?.[CLIENT_CAPABILITIES_META_KEY];
return (
typeof version === "string" &&
PROCESS_CAPTURE_ELICITATION_POLICY.protocolVersions.some(
(supported) => supported === version,
) &&
isRecord(capabilities) &&
isRecord(capabilities.elicitation) &&
capabilities.elicitation.form !== undefined
);
},
now: Date.now,
consumedNonces: new Map<string, number>(),
};
registerBinaryAnalysisTools(toolContext);
registerObservationTools(toolContext);
let optionalProviders:
| ReturnType<NonNullable<CreateServerOptions["loadOptionalProviders"]>>
| undefined;
const loadOptionalProviders = () => {
optionalProviders ??= options
.loadOptionalProviders?.()
.catch((cause: unknown) => {
optionalProviders = undefined;
throw cause;
});
return optionalProviders;
};
const lazyTools = new LazyToolCatalog(
server,
async (kind) => {
const needsObservationProviders =
kind === "browser-provider" ||
kind === "electron-provider" ||
kind === "runtime-provider";
const [{ hydrateServerToolFamily }, loadedOptionalProviders] =
await Promise.all([
import("./hydrateServerTools.js"),
needsObservationProviders ? loadOptionalProviders() : undefined,
]);
const hydratedOptions =
loadedOptionalProviders === undefined
? options
: { ...options, ...loadedOptionalProviders };
await hydrateServerToolFamily({
kind,
server,
analysis,
session,
options: hydratedOptions,
context: {
logger: toolLogger,
permissionAuthority,
activeTarget,
recordEvidence,
recordEvidenceWithUnknown,
availabilityPolicy: dynamicTools?.policy,
startedAt,
processCaptureElicitation,
},
});
},
session !== undefined,
);
lazyTools.register();
registerGuidedPrompts(server, analysis, session);
if (session !== undefined) {
registerEvidenceResources(server, session);
registerSessionTools(server, session, toolLogger, {
...options,
...(dynamicTools === undefined
? {}
: { availabilityPolicy: dynamicTools.policy }),
...(permissionAuthority === undefined ? {} : { permissionAuthority }),
startedAt,
processCaptureElicitation: {
stateCodec: processCaptureStateCodec,
supported: (context) => {
const envelope = context.mcpReq.envelope;
const version = envelope?.[PROTOCOL_VERSION_META_KEY];
const capabilities = envelope?.[CLIENT_CAPABILITIES_META_KEY];
return (
typeof version === "string" &&
PROCESS_CAPTURE_ELICITATION_POLICY.protocolVersions.some(
(supported) => supported === version,
) &&
isRecord(capabilities) &&
isRecord(capabilities.elicitation) &&
capabilities.elicitation.form !== undefined
);
},
now: Date.now,
consumedNonces: new Map(),
},
});
}
dynamicTools?.controller.synchronize();
const close = server.close.bind(server);
server.close = async () => {
await lazyTools.close();
await close();
};
return server;
};
@@ -243,7 +272,8 @@ const registerServerIdentityResource = (
description: "Live package, SDK, protocol, and catalog identity.",
mimeType: "application/json",
},
(uri) => {
async (uri) => {
const { createServerIdentity } = await import("../serverIdentity.js");
const client = server.server.getClientVersion();
const protocolVersion = server.server.getNegotiatedProtocolVersion();
return {
@@ -267,154 +297,5 @@ const registerServerIdentityResource = (
);
};
interface ServerToolContext extends ReturnType<typeof createSessionRecorders> {
readonly server: McpServer;
readonly analysis: AnalysisOperationPort;
readonly session: BinarySessionPort | undefined;
readonly options: CreateServerOptions;
readonly logger: Logger;
readonly permissionAuthority: PermissionAuthority | undefined;
}
const registerBinaryAnalysisTools = ({
server,
analysis,
session,
options,
logger,
permissionAuthority,
activeTarget,
recordEvidence,
recordEvidenceWithUnknown,
}: ServerToolContext): void => {
const recordUnknown =
session === undefined
? undefined
: (input: Parameters<typeof session.recordUnknown>[0]) =>
session.recordUnknown(input);
registerOfficialTools(server, analysis, {
logger,
activeTarget,
recordEvidence,
recordUnknown,
});
registerEnhancedTools(server, analysis, {
logger,
activeTarget,
analysisProfile:
session === undefined ? undefined : () => session.analysisProfile(),
recordEvidence,
recordUnknown,
});
registerNativeTools(server, analysis, {
logger,
activeTarget,
recordEvidence,
});
registerArtifactTools(server, analysis, {
logger,
activeTarget,
recordEvidence,
...(permissionAuthority === undefined ? {} : { permissionAuthority }),
});
registerManagedTools(server, analysis, {
logger,
activeTarget,
recordEvidence,
session,
});
if (session !== undefined)
registerManagedWorkflowTools(server, {
logger,
recordEvidence,
recordEvidenceWithUnknown,
session,
runtime: {
policy: options.managedRuntimePolicy ?? {
enabled: false,
roots: [],
executablePath: "/usr/bin/dotnet",
},
authority: permissionAuthority,
},
});
};
const registerObservationTools = ({
server,
session,
options,
logger,
permissionAuthority,
recordEvidence,
recordEvidenceWithUnknown,
}: ServerToolContext): void => {
registerBrowserTools(server, {
logger,
browser: options.browserObservation,
permissionAuthority,
recordEvidence,
});
registerBrowserScenarioTool(server, {
logger,
provider: options.browserScenarioCapture,
permissionAuthority,
recordEvidence,
});
registerElectronTools(server, {
logger,
electron: options.electronObservation,
permissionAuthority,
recordEvidence,
});
registerJavaScriptRuntimeObservationTools(server, {
logger,
runtime: options.javascriptRuntimeObservation,
permissionAuthority,
recordEvidence,
});
registerApplicationTools(server, {
logger,
recordEvidence,
recordEvidenceWithUnknown,
evidenceLookup:
session === undefined
? undefined
: (evidenceId) => session.evidenceById(evidenceId),
evidenceFilePolicy: options.evidenceFilePolicy ?? {
roots: [],
maxBytes: 1,
maxDepth: 1,
maxStringLength: 1,
maxNodes: 1,
},
permissionAuthority,
retainCoverageWorkspace:
session === undefined
? undefined
: (workspace) => {
const retained =
session.retainReconstructionCoverageWorkspace(workspace);
if (retained === "added") server.sendResourceListChanged();
return retained;
},
replay: {
policy: options.javascriptReplayPolicy ?? {
enabled: false,
roots: [],
nodePath: process.execPath,
bubblewrapPath: "/usr/bin/bwrap",
systemdRunPath: "/usr/bin/systemd-run",
systemctlPath: "/usr/bin/systemctl",
shellPath: "/usr/bin/bash",
},
host: options.javascriptReplayHost ?? new SystemJavaScriptReplayHost(),
runner:
options.javascriptReplayRunner ?? new LinuxJavaScriptReplayRunner(),
authority: permissionAuthority,
},
});
};
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
+300
View File
@@ -0,0 +1,300 @@
import type { McpServer } from "@modelcontextprotocol/server";
import type { AnalysisOperationPort } from "../application/AnalysisProvider.js";
import type { BinarySessionPort } from "../application/BinarySession.js";
import type { PermissionAuthority } from "../application/PermissionAuthority.js";
import type { BinaryTarget } from "../domain/binaryTarget.js";
import type { Logger } from "../logger.js";
import type { ToolKind } from "../contracts/toolContractTypes.js";
import type { CreateServerOptions } from "./createServer.js";
import type { ProcessCaptureElicitation } from "./ProcessCaptureElicitation.js";
import type { SessionAvailability } from "./sessionAvailabilityPolicy.js";
interface HydratedToolContext {
readonly logger: Logger;
readonly permissionAuthority: PermissionAuthority | undefined;
readonly activeTarget: (() => BinaryTarget | undefined) | undefined;
readonly recordEvidence: BinarySessionPort["recordEvidence"] | undefined;
readonly recordEvidenceWithUnknown:
| BinarySessionPort["recordEvidenceWithUnknown"]
| undefined;
readonly availabilityPolicy: (() => SessionAvailability) | undefined;
readonly startedAt: string;
readonly processCaptureElicitation: ProcessCaptureElicitation;
}
export interface HydrateServerToolFamilyInput {
readonly kind: ToolKind;
readonly server: McpServer;
readonly analysis: AnalysisOperationPort;
readonly session: BinarySessionPort | undefined;
readonly options: CreateServerOptions;
readonly context: HydratedToolContext;
}
/**
* Install production callbacks on generated lazy tool registrations.
*/
export const hydrateServerToolFamily = async ({
kind,
server,
analysis,
session,
options,
context,
}: HydrateServerToolFamilyInput): Promise<void> => {
const toolContext = {
server,
analysis,
session,
options,
logger: context.logger,
permissionAuthority: context.permissionAuthority,
activeTarget: context.activeTarget,
recordEvidence: context.recordEvidence,
recordEvidenceWithUnknown: context.recordEvidenceWithUnknown,
};
switch (kind) {
case "official-proxy": {
const { registerOfficialTools } = await import(
"./registerOfficialTools.js"
);
registerOfficialTools(server, analysis, analysisOptions(toolContext));
return;
}
case "enhanced": {
const { registerEnhancedTools } = await import(
"./registerEnhancedTools.js"
);
registerEnhancedTools(server, analysis, enhancedOptions(toolContext));
return;
}
case "native-provider": {
const { registerNativeTools } = await import("./registerNativeTools.js");
registerNativeTools(server, analysis, evidenceOptions(toolContext));
return;
}
case "artifact-provider": {
const { registerArtifactTools } = await import(
"./registerArtifactTools.js"
);
registerArtifactTools(server, analysis, {
...evidenceOptions(toolContext),
...(context.permissionAuthority === undefined
? {}
: { permissionAuthority: context.permissionAuthority }),
});
return;
}
case "managed-provider": {
const { registerManagedTools } = await import(
"./registerManagedTools.js"
);
registerManagedTools(server, analysis, {
...evidenceOptions(toolContext),
session,
});
return;
}
case "application":
await registerApplicationFamily(toolContext);
return;
case "browser-provider":
case "electron-provider":
case "runtime-provider": {
await hydrateObservationFamily(kind, toolContext);
return;
}
case "session": {
if (session === undefined)
throw new TypeError("Session tools require a binary session");
const { registerSessionTools } = await import(
"./registerSessionTools.js"
);
registerSessionTools(server, session, context.logger, {
...options,
...(context.availabilityPolicy === undefined
? {}
: { availabilityPolicy: context.availabilityPolicy }),
...(context.permissionAuthority === undefined
? {}
: { permissionAuthority: context.permissionAuthority }),
startedAt: context.startedAt,
processCaptureElicitation: context.processCaptureElicitation,
});
}
}
};
const hydrateObservationFamily = async (
kind: "browser-provider" | "electron-provider" | "runtime-provider",
context: ServerToolContext,
): Promise<void> => {
const common = {
logger: context.logger,
permissionAuthority: context.permissionAuthority,
recordEvidence: context.recordEvidence,
};
if (kind === "browser-provider") {
const [{ registerBrowserTools }, { registerBrowserScenarioTool }] =
await Promise.all([
import("./registerBrowserTools.js"),
import("./registerBrowserScenarioTool.js"),
]);
registerBrowserTools(context.server, {
...common,
browser: context.options.browserObservation,
});
registerBrowserScenarioTool(context.server, {
...common,
provider: context.options.browserScenarioCapture,
});
return;
}
if (kind === "electron-provider") {
const { registerElectronTools } = await import(
"./registerElectronTools.js"
);
registerElectronTools(context.server, {
...common,
electron: context.options.electronObservation,
});
return;
}
const { registerJavaScriptRuntimeObservationTools } = await import(
"./registerJavaScriptRuntimeObservationTools.js"
);
registerJavaScriptRuntimeObservationTools(context.server, {
...common,
runtime: context.options.javascriptRuntimeObservation,
});
};
interface ServerToolContext {
readonly server: McpServer;
readonly analysis: AnalysisOperationPort;
readonly session: BinarySessionPort | undefined;
readonly options: CreateServerOptions;
readonly logger: Logger;
readonly permissionAuthority: PermissionAuthority | undefined;
readonly activeTarget: (() => BinaryTarget | undefined) | undefined;
readonly recordEvidence: BinarySessionPort["recordEvidence"] | undefined;
readonly recordEvidenceWithUnknown:
| BinarySessionPort["recordEvidenceWithUnknown"]
| undefined;
}
const analysisOptions = ({
logger,
activeTarget,
recordEvidence,
session,
}: ServerToolContext) => {
const recordUnknown =
session === undefined
? undefined
: (input: Parameters<typeof session.recordUnknown>[0]) =>
session.recordUnknown(input);
return {
logger,
activeTarget,
recordEvidence,
recordUnknown,
};
};
const enhancedOptions = (context: ServerToolContext) => ({
...analysisOptions(context),
analysisProfile:
context.session === undefined
? undefined
: () => context.session?.analysisProfile(),
});
const evidenceOptions = ({
logger,
activeTarget,
recordEvidence,
}: ServerToolContext) => ({
logger,
activeTarget,
recordEvidence,
});
const registerApplicationFamily = async ({
server,
session,
options,
logger,
permissionAuthority,
recordEvidence,
recordEvidenceWithUnknown,
}: ServerToolContext): Promise<void> => {
const [
{ registerApplicationTools },
{ registerManagedWorkflowTools },
{ LinuxJavaScriptReplayRunner },
{ SystemJavaScriptReplayHost },
] = await Promise.all([
import("./registerApplicationTools.js"),
import("./registerManagedWorkflowTools.js"),
import("../replay/LinuxJavaScriptReplayRunner.js"),
import("../replay/SystemJavaScriptReplayHost.js"),
]);
if (session !== undefined)
registerManagedWorkflowTools(server, {
logger,
recordEvidence,
recordEvidenceWithUnknown,
session,
runtime: {
policy: options.managedRuntimePolicy ?? {
enabled: false,
roots: [],
executablePath: "/usr/bin/dotnet",
},
authority: permissionAuthority,
},
});
registerApplicationTools(server, {
logger,
recordEvidence,
recordEvidenceWithUnknown,
evidenceLookup:
session === undefined
? undefined
: (evidenceId) => session.evidenceById(evidenceId),
evidenceFilePolicy: options.evidenceFilePolicy ?? {
roots: [],
maxBytes: 1,
maxDepth: 1,
maxStringLength: 1,
maxNodes: 1,
},
permissionAuthority,
retainCoverageWorkspace:
session === undefined
? undefined
: (workspace) => {
const retained =
session.retainReconstructionCoverageWorkspace(workspace);
if (retained === "added") server.sendResourceListChanged();
return retained;
},
replay: {
policy: options.javascriptReplayPolicy ?? {
enabled: false,
roots: [],
nodePath: process.execPath,
bubblewrapPath: "/usr/bin/bwrap",
systemdRunPath: "/usr/bin/systemd-run",
systemctlPath: "/usr/bin/systemctl",
shellPath: "/usr/bin/bash",
},
host: options.javascriptReplayHost ?? new SystemJavaScriptReplayHost(),
runner:
options.javascriptReplayRunner ?? new LinuxJavaScriptReplayRunner(),
authority: permissionAuthority,
},
});
};
+22 -5
View File
@@ -223,6 +223,12 @@ const ENHANCED_INPUT_PARSERS: {
input,
"analyze_function",
),
inspect_native_api: (input) =>
safeParseToolInput(
enhancedInputSchemas.inspect_native_api,
input,
"inspect_native_api",
),
trace_feature: (input) =>
safeParseToolInput(
enhancedInputSchemas.trace_feature,
@@ -293,6 +299,11 @@ const parseEnhancedCall = (
ENHANCED_INPUT_PARSERS.analyze_function(input),
(value) => ({ name, input: value }),
);
case "inspect_native_api":
return mapEnhancedCall(
ENHANCED_INPUT_PARSERS.inspect_native_api(input),
(value) => ({ name, input: value }),
);
case "trace_feature":
return mapEnhancedCall(
ENHANCED_INPUT_PARSERS.trace_feature(input),
@@ -335,9 +346,12 @@ const recordWorkflowUnknowns = ({
| ReturnType<BinarySessionPort["recordUnknown"]>
| { readonly ok: true; readonly value: null } => {
if (
!["trace_feature", "find_code_for_string", "trace_call_path"].includes(
name,
) ||
![
"trace_feature",
"find_code_for_string",
"trace_call_path",
"inspect_native_api",
].includes(name) ||
input.unknown_registry_approved !== true ||
recordUnknown === undefined ||
typeof result !== "object" ||
@@ -352,7 +366,7 @@ const recordWorkflowUnknowns = ({
approved: true,
question,
severity: "medium",
domain: "control-flow",
domain: name === "inspect_native_api" ? "native-api" : "control-flow",
supporting_evidence_ids: [evidenceId],
contradicting_evidence_ids: [],
required_authority: "shipped-artifact",
@@ -361,7 +375,10 @@ const recordWorkflowUnknowns = ({
recommended_probes: [
{
operation: name,
rationale: "Continue with a larger bounded budget.",
rationale:
name === "inspect_native_api"
? "Confirm the unsupported boundary with a capable provider or ABI probe."
: "Continue with a larger bounded budget.",
},
],
relationships: [],
+42 -12
View File
@@ -2,6 +2,15 @@ import { z } from "zod";
import type { ToolContract } from "../contracts/toolContracts.js";
const ADVERTISED_INPUT_SCHEMAS = new WeakMap<
z.ZodObject,
Readonly<Record<string, unknown>>
>();
const ADVERTISED_OUTPUT_SCHEMAS = new WeakMap<
z.ZodObject,
Readonly<Record<string, unknown>>
>();
/** Project the public contract fields accepted by MCP tool registration. */
export const toolRegistrationOptions = (contract: ToolContract) => ({
title: contract.title,
@@ -22,10 +31,8 @@ const advertisedInputSchema = (schema: z.ZodObject) => ({
vendor: "rea",
validate: (input: unknown) => ({ value: input }),
jsonSchema: {
input: () =>
describeProperties(closeObjectSchemas(inputJsonSchema(schema))),
output: () =>
describeProperties(closeObjectSchemas(inputJsonSchema(schema))),
input: () => advertisedInputJsonSchema(schema),
output: () => advertisedInputJsonSchema(schema),
},
},
});
@@ -41,19 +48,42 @@ const advertisedOutputSchema = (schema: z.ZodObject) => {
"~standard": {
...standard,
jsonSchema: {
input: () =>
describeRootProperties(
closeObjectSchemas(converter({ target: "draft-2020-12" })),
),
output: () =>
describeRootProperties(
closeObjectSchemas(converter({ target: "draft-2020-12" })),
),
input: () => advertisedOutputJsonSchema(schema, converter),
output: () => advertisedOutputJsonSchema(schema, converter),
},
},
};
};
const advertisedInputJsonSchema = (
schema: z.ZodObject,
): Readonly<Record<string, unknown>> =>
cachedJsonSchema(ADVERTISED_INPUT_SCHEMAS, schema, () =>
describeProperties(closeObjectSchemas(inputJsonSchema(schema))),
);
const advertisedOutputJsonSchema = (
schema: z.ZodObject,
converter: NonNullable<z.ZodObject["~standard"]["jsonSchema"]>["output"],
): Readonly<Record<string, unknown>> =>
cachedJsonSchema(ADVERTISED_OUTPUT_SCHEMAS, schema, () =>
describeRootProperties(
closeObjectSchemas(converter({ target: "draft-2020-12" })),
),
);
const cachedJsonSchema = (
cache: WeakMap<z.ZodObject, Readonly<Record<string, unknown>>>,
schema: z.ZodObject,
create: () => Record<string, unknown>,
): Readonly<Record<string, unknown>> => {
const cached = cache.get(schema);
if (cached !== undefined) return cached;
const created = create();
cache.set(schema, created);
return created;
};
const inputJsonSchema = (schema: z.ZodObject): Record<string, unknown> => {
const converter = schema["~standard"].jsonSchema?.input;
if (converter === undefined)
-6
View File
@@ -107,12 +107,6 @@ describe("Hopper bridge truthfulness", () => {
expect(bridgeSource).toContain("_invalidate_search_inventory(document)");
});
it("classifies bridge exceptions without forwarding tracebacks", () => {
expect(bridgeSource).toContain("def _diagnostic_type(error):");
expect(bridgeSource).toContain('"type": _diagnostic_type(error)');
expect(bridgeSource).not.toContain("traceback.format_exc");
});
it("stops background analysis and closes the session-owned document", () => {
expect(bridgeSource).toContain("def _session_document():");
expect(bridgeSource).toContain("os.path.realpath(REA_TARGET_PATH)");
+152
View File
@@ -0,0 +1,152 @@
import { describe, expect, it } from "vitest";
import {
analyzeBytecodeArtifacts,
bytecodeAnalysisSchema,
classifyProvenance,
detectArtifactFormat,
detectBytecodeFamily,
type BytecodeArtifact,
type BytecodeSymbol,
} from "../src/domain/bytecodeProvider.js";
describe("bytecode provider", () => {
it("detects JVM family from extensions", () => {
expect(detectBytecodeFamily("App.class")).toBe("jvm");
expect(detectBytecodeFamily("app.jar")).toBe("jvm");
expect(detectBytecodeFamily("webapp.war")).toBe("jvm");
expect(detectBytecodeFamily("app.ear")).toBe("jvm");
});
it("detects Python family from extensions", () => {
expect(detectBytecodeFamily("module.pyc")).toBe("python");
expect(detectBytecodeFamily("module.pyo")).toBe("python");
expect(detectBytecodeFamily("package.whl")).toBe("python");
expect(detectBytecodeFamily("app.pyz")).toBe("python");
expect(detectBytecodeFamily("app.zipapp")).toBe("python");
});
it("returns null for unknown extensions", () => {
expect(detectBytecodeFamily("readme.txt")).toBeNull();
expect(detectBytecodeFamily("script.js")).toBeNull();
});
it("detects artifact format from path", () => {
expect(detectArtifactFormat("App.class")).toBe("class");
expect(detectArtifactFormat("lib.jar")).toBe("jar");
expect(detectArtifactFormat("web.war")).toBe("war");
expect(detectArtifactFormat("mod.pyc")).toBe("pyc");
expect(detectArtifactFormat("pkg.whl")).toBe("whl");
expect(detectArtifactFormat("unknown.xyz")).toBeNull();
});
it("classifies standard library provenance", () => {
expect(classifyProvenance("/usr/lib/stdlib", "java.lang.String")).toBe(
"standard_library",
);
expect(
classifyProvenance("/lib/python3.10/site-packages/numpy", "__init__"),
).toBe("standard_library");
});
it("classifies generated provenance", () => {
expect(classifyProvenance("/build/generated/src", "GeneratedClass")).toBe(
"generated",
);
expect(
classifyProvenance("/__pycache__/module.cpython-310.pyc", "module"),
).toBe("generated");
});
it("classifies vendored provenance", () => {
expect(classifyProvenance("/vendor/libs/lib.jar", "VendorLib")).toBe(
"vendored",
);
expect(classifyProvenance("/third_party/pkg/module.pyc", "module")).toBe(
"vendored",
);
});
it("classifies application provenance", () => {
expect(classifyProvenance("/src/main/App.java", "com.example.App")).toBe(
"application",
);
expect(classifyProvenance("/src/app.py", "main")).toBe("application");
});
it("analyzes bytecode artifacts and computes summary", () => {
const artifacts: BytecodeArtifact[] = [
{
path: "src/App.class",
family: "jvm",
format: "class",
size: 1000,
digest: "a".repeat(64),
symbols: [
{
name: "com.example.App",
raw_location: "0x1000",
kind: "class",
bytecode_version: 52,
provenance: "application",
is_public: true,
},
{
name: "java.lang.String",
raw_location: "0x2000",
kind: "class",
bytecode_version: 52,
provenance: "standard_library",
is_public: true,
},
],
is_standard_library: false,
is_generated: false,
is_vendored: false,
},
{
path: "lib/generated/Generated.class",
family: "jvm",
format: "class",
size: 500,
digest: "b".repeat(64),
symbols: [
{
name: "GeneratedClass",
raw_location: "0x3000",
kind: "class",
bytecode_version: 52,
provenance: "generated",
is_public: false,
},
],
is_standard_library: false,
is_generated: true,
is_vendored: false,
},
];
const result = analyzeBytecodeArtifacts(artifacts, "jvm");
expect(result.family).toBe("jvm");
expect(result.total_symbols).toBe(3);
expect(result.application_symbols).toBe(1);
expect(result.standard_library_symbols).toBe(1);
expect(result.generated_symbols).toBe(1);
expect(result.vendored_symbols).toBe(0);
// Validate schema
const parsed = bytecodeAnalysisSchema.safeParse(result);
expect(parsed.success).toBe(true);
});
it("uses BytecodeSymbol type", () => {
const sym: BytecodeSymbol = {
name: "com.example.Test",
raw_location: "0x100",
kind: "class",
bytecode_version: 52,
provenance: "application",
is_public: true,
};
expect(sym.provenance).toBe("application");
});
});
+73 -1
View File
@@ -19,10 +19,82 @@ __attribute__((noinline, used)) int rea_ghidra_inventory_indirect(
return callback(value);
}
__attribute__((noinline, used)) int rea_ghidra_inventory_switch(
unsigned int selector, int value) {
switch (selector) {
case 0:
return rea_ghidra_inventory_leaf(value);
case 1:
return rea_ghidra_inventory_leaf(value + 3);
case 2:
return rea_ghidra_inventory_leaf(value - 5);
case 3:
return rea_ghidra_inventory_leaf(value * 2);
case 4:
return rea_ghidra_inventory_leaf(value ^ 0x55);
case 5:
return rea_ghidra_inventory_leaf(value | 0x20);
case 6:
return rea_ghidra_inventory_leaf(value & 0x3f);
case 7:
return rea_ghidra_inventory_leaf(value + rea_ghidra_inventory_global);
default:
return -1;
}
}
__attribute__((noinline, used)) int rea_ghidra_inventory_dense_switch(
unsigned int selector, int value) {
switch (selector) {
case 0: return rea_ghidra_inventory_leaf(value + 0);
case 1: return rea_ghidra_inventory_leaf(value + 1);
case 2: return rea_ghidra_inventory_leaf(value + 2);
case 3: return rea_ghidra_inventory_leaf(value + 3);
case 4: return rea_ghidra_inventory_leaf(value + 4);
case 5: return rea_ghidra_inventory_leaf(value + 5);
case 6: return rea_ghidra_inventory_leaf(value + 6);
case 7: return rea_ghidra_inventory_leaf(value + 7);
case 8: return rea_ghidra_inventory_leaf(value + 8);
case 9: return rea_ghidra_inventory_leaf(value + 9);
case 10: return rea_ghidra_inventory_leaf(value + 10);
case 11: return rea_ghidra_inventory_leaf(value + 11);
case 12: return rea_ghidra_inventory_leaf(value + 12);
case 13: return rea_ghidra_inventory_leaf(value + 13);
case 14: return rea_ghidra_inventory_leaf(value + 14);
case 15: return rea_ghidra_inventory_leaf(value + 15);
case 16: return rea_ghidra_inventory_leaf(value + 16);
case 17: return rea_ghidra_inventory_leaf(value + 17);
case 18: return rea_ghidra_inventory_leaf(value + 18);
case 19: return rea_ghidra_inventory_leaf(value + 19);
case 20: return rea_ghidra_inventory_leaf(value + 20);
case 21: return rea_ghidra_inventory_leaf(value + 21);
case 22: return rea_ghidra_inventory_leaf(value + 22);
case 23: return rea_ghidra_inventory_leaf(value + 23);
case 24: return rea_ghidra_inventory_leaf(value + 24);
case 25: return rea_ghidra_inventory_leaf(value + 25);
case 26: return rea_ghidra_inventory_leaf(value + 26);
case 27: return rea_ghidra_inventory_leaf(value + 27);
case 28: return rea_ghidra_inventory_leaf(value + 28);
case 29: return rea_ghidra_inventory_leaf(value + 29);
case 30: return rea_ghidra_inventory_leaf(value + 30);
case 31: return rea_ghidra_inventory_leaf(value + 31);
case 32: return rea_ghidra_inventory_leaf(value + 32);
case 33: return rea_ghidra_inventory_leaf(value + 33);
case 34: return rea_ghidra_inventory_leaf(value + 34);
case 35: return rea_ghidra_inventory_leaf(value + 35);
case 36: return rea_ghidra_inventory_leaf(value + 36);
case 37: return rea_ghidra_inventory_leaf(value + 37);
case 38: return rea_ghidra_inventory_leaf(value + 38);
case 39: return rea_ghidra_inventory_leaf(value + 39);
default: return -1;
}
}
__attribute__((noinline, used)) int rea_ghidra_inventory_entry(void) {
puts("REA_GHIDRA_INVENTORY_ENTRY");
return rea_ghidra_inventory_branch(35) +
rea_ghidra_inventory_indirect(rea_ghidra_inventory_leaf, 0);
rea_ghidra_inventory_indirect(rea_ghidra_inventory_leaf, 0) +
rea_ghidra_inventory_dense_switch(100, 0) + 1;
}
int main(void) { return rea_ghidra_inventory_entry() == 49 ? 0 : 1; }

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